Compare commits

..

14 Commits

Author SHA1 Message Date
Paul Payne
e2aa16e679 Add external-dns annotations to openproject. 2026-01-08 01:17:20 +00:00
Paul Payne
37dafcd24d Remove stale apps from readme. 2026-01-04 23:56:53 +00:00
Paul Payne
b6d88e79ac Mastodon vapid init. 2026-01-04 23:56:37 +00:00
Paul Payne
963929475c Update CLAUDE.md. 2026-01-04 19:36:54 +00:00
Paul Payne
39095e76d2 Add Matrix. 2026-01-04 19:36:40 +00:00
Paul Payne
d756126a34 Add Mastodon. 2026-01-04 19:36:31 +00:00
Paul Payne
f17fea6910 Add Lemmy. 2026-01-04 19:36:23 +00:00
Paul Payne
0ba33a315d Add Decidim. 2026-01-04 19:36:15 +00:00
Paul Payne
12706ac331 Fix db health checks in openproject. 2026-01-01 22:21:36 +00:00
Paul Payne
a159c90816 Update listmonk icon. 2026-01-01 20:57:14 +00:00
Paul Payne
32498c73b8 Get discourse working. 2026-01-01 20:57:03 +00:00
Paul Payne
c93198d13a Add loomio. 2026-01-01 20:56:41 +00:00
Paul Payne
434769ac7a Adds is attribute to manifests. 2025-12-31 08:15:17 +00:00
Paul Payne
d1304a2630 v2 app deployment--templating mainly in manifest now. 2025-12-31 06:53:17 +00:00
138 changed files with 3374 additions and 633 deletions

View File

@@ -14,7 +14,7 @@ Each app directory must contain:
2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels 2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.) 3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.)
### App Manifest (`manifest.yaml`) ## App Manifest (`manifest.yaml`)
The manifest defines the app's metadata, dependencies, configuration schema, and secret requirements. The manifest defines the app's metadata, dependencies, configuration schema, and secret requirements.
@@ -22,12 +22,14 @@ This is the contents of an example `manifest.yaml` file for an app named "immich
```yaml ```yaml
name: immich name: immich
is: immich
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely. description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
version: 1.0.0 version: 1.0.0
icon: https://immich.app/assets/images/logo.png icon: https://immich.app/assets/images/logo.png
requires: requires:
- name: redis - name: pg
- name: postgres alias: db # Use a different reference name in templates
- name: redis # 'alias' and 'installedAs' default to 'name' value
defaultConfig: defaultConfig:
serverImage: ghcr.io/immich-app/immich-server:release serverImage: ghcr.io/immich-app/immich-server:release
mlImage: ghcr.io/immich-app/immich-machine-learning:release mlImage: ghcr.io/immich-app/immich-machine-learning:release
@@ -36,28 +38,195 @@ defaultConfig:
mlPort: 3003 mlPort: 3003
storage: 250Gi storage: 250Gi
cacheStorage: 10Gi cacheStorage: 10Gi
redisHostname: redis.redis.svc.cluster.local redisHostname: "{{ .apps.redis.host }}" # Can reference 'requires' app configurations
dbHostname: postgres.postgres.svc.cluster.local dbHostname: "{{ .apps.pg.host }}"
dbUsername: immich db: # Configuration can be nested
name: immich
user: immich
host: "{{ .apps.pg.host }}"
port: "{{ .apps.pg.port }}"
domain: immich.{{ .cloud.domain }} domain: immich.{{ .cloud.domain }}
defaultSecrets: defaultSecrets:
- apps.immich.dbPassword - key: password # Random value will be generated if empty
- apps.postgres.password - key: dbUrl
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30" # Can reference secrets and config as long as they have been defined before this line. Reference config with {{ .app.? }} and secrets with {{ .secrets.? }}
requiredSecrets:
- db.password # References postgres app via 'db' alias
- redis.auth # References redis app via 'redis' name (no alias)
``` ```
#### Manifest Fields ### Manifest Fields
| Field | Required | Description | | Field | Required | Description |
|-------|----------|-------------| |-------|----------|-------------|
| `name` | Yes | App identifier (must match directory name) | | `name` | Yes | App identifier (must match directory name) |
| `is` | Yes | Unique id for this app. Used for `requires` mapping |
| `description` | Yes | Brief app description shown in listings | | `description` | Yes | Brief app description shown in listings |
| `version` | Yes | App version (follow upstream versioning) | | `version` | Yes | App version (follow upstream versioning) |
| `icon` | No | URL to app icon for UI display | | `icon` | No | URL to app icon for UI display |
| `requires` | No | List of dependency apps (e.g., `postgres`, `redis`) | | `requires` | No | List of dependency apps with optional aliases |
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` | | `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
| `defaultSecrets` | No | List of secrets in dotted-path format (e.g., `apps.appname.dbPassword`) | | `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) |
**Important:** All configuration keys referenced in templates (via `{{ .apps.appname.key }}`) must be defined in `defaultConfig` or be standard Wild Cloud variables. ### Dependency Configuration
- Each dependency in `requires` can have:
- `name`: The app name to depend on (any app with a matching `is` field can satisfy this requirement)
- `alias`: Optional reference name for templates (defaults to `name`)
### Manifest Template Variables (configuration and secrets)
#### Manifest Template Variable Sources
1. Standard Wild Cloud variables: `{{ .cloud.* }}`, `{{ .cluster.* }}`, `{{ .operator.* }}`
2. App-specific variables: `{{ .app.* }}` - resolved from current app's config
3. Dependency variables: `{{ .apps.<ref>.* }}` - resolved using app reference mapping
4. App-specific secrets (in 'defaultSecrets' ONLY): `{{ secrets.* }}`
#### Available Configuration Variiables
Here's a comprehensive rundown of all config variables that get set during cluster and service setup in config.yaml:
##### operator (Set during initial setup)
- operator.email - Email for cluster operator/admin
##### cloud (Infrastructure-level settings)
###### DNS Configuration:
- cloud.dns.ip - IP address of the DNS server (Wild Central)
- cloud.dns.externalResolver - External DNS resolver (e.g., 1.1.1.1, 8.8.8.8)
###### Network Configuration:
- cloud.router.ip - Router gateway IP
- cloud.router.dynamicDns - Dynamic DNS hostname (optional)
- cloud.dhcpRange - DHCP range for the network (e.g., "192.168.8.34,192.168.8.79")
- cloud.dnsmasq.interface - Network interface for dnsmasq
###### Domain Configuration:
- cloud.baseDomain - Base domain for the cloud (e.g., "payne.io")
- cloud.domain - Full cloud domain (e.g., "cloud2.payne.io")
- cloud.internalDomain - Internal cluster domain (e.g., "internal.cloud2.payne.io")
###### Storage Configuration (NFS Service):
- cloud.nfs.host - NFS server hostname/IP
- cloud.nfs.mediaPath - NFS export path for media storage
- cloud.nfs.storageCapacity - NFS storage capacity (e.g., "50Gi", "1Ti")
###### Registry Configuration (Docker Registry Service):
- cloud.dockerRegistryHost - Docker registry hostname (e.g., "registry.internal.cloud2.payne.io")
##### SMTP Configuration (SMTP Service):
- cloud.smtp.host - SMTP server hostname
- cloud.smtp.port - SMTP port (typically "465" or "587")
- cloud.smtp.user - SMTP username
- cloud.smtp.from - Default 'from' email address
- cloud.smtp.tls - Enable TLS (true/false)
- cloud.smtp.startTls - Enable STARTTLS (true/false)
###### Backup Configuration:
- cloud.backup.root - Root path for backups
##### cluster (Kubernetes cluster settings)
###### Basic Cluster Info:
- cluster.name - Cluster name identifier
- cluster.hostnamePrefix - Prefix for node hostnames
###### Node Configuration:
- cluster.nodes.talos.version - Talos Linux version (e.g., "v1.11.5")
- cluster.nodes.talos.schematicId - Talos Image Factory schematic ID
- cluster.nodes.control.vip - Virtual IP for control plane
- cluster.nodes.active.* - Individual node configurations with:
- role - "controlplane" or "worker"
- interface - Network interface name
- disk - Disk device path
- currentIp - Current IP address
- targetIp - Target IP address
- configured - Configuration status
- applied - Applied status
- maintenance - Maintenance mode
- schematicId - Node-specific schematic ID
- version - Node-specific Talos version
###### MetalLB Service:
- cluster.ipAddressPool - IP range for MetalLB (e.g., "192.168.8.80-192.168.8.89")
- cluster.loadBalancerIp - Primary load balancer IP (e.g., "192.168.8.80")
###### Cert-Manager Service:
- cluster.certManager.cloudflare.domain - Cloudflare domain for DNS-01 challenge
- cluster.certManager.cloudflare.zoneID - Cloudflare zone ID
###### ExternalDNS Service:
- cluster.externalDns.ownerId - Unique identifier for this cluster's DNS records
###### Docker Registry Service:
- cluster.dockerRegistry.storage - Storage size for registry (e.g., "10Gi")
##### apps (Application configurations)
Each app added to the cluster gets its own section under apps.<app-name> with app-specific configuration from the app's manifest. Common patterns include:
Standard app fields:
- apps.<name>.namespace - Kubernetes namespace
- apps.<name>.domain - App domain (e.g., "ghost.cloud2.payne.io")
- apps.<name>.externalDnsDomain - Domain for external DNS
- apps.<name>.tlsSecretName - TLS certificate secret name
- apps.<name>.image - Container image
- apps.<name>.port - Service port
- apps.<name>.storage - Persistent volume size
- apps.<name>.timezone - Timezone setting
Database-dependent apps:
- apps.<name>.dbHost / dbHostname - Database hostname
- apps.<name>.dbPort - Database port
- apps.<name>.dbName - Database name
- apps.<name>.dbUser / dbUsername - Database user
SMTP-enabled apps:
- apps.<name>.smtp.host - SMTP server
- apps.<name>.smtp.port - SMTP port
- apps.<name>.smtp.user - SMTP username
- apps.<name>.smtp.from - From address
- apps.<name>.smtp.tls - TLS enabled
- apps.<name>.smtp.startTls - STARTTLS enabled
Configuration Flow
1. Initial Setup: operator.email, basic cloud.* settings
2. Cluster Bootstrap: cluster.name, cluster.nodes.* settings
3. Infrastructure Services: Each service prompts for its serviceConfig from its manifest
- MetalLB → cluster.ipAddressPool, cluster.loadBalancerIp
- Cert-Manager → cluster.certManager.*
- ExternalDNS → cluster.externalDns.ownerId
- NFS → cloud.nfs.*
- Docker Registry → cloud.dockerRegistryHost, cluster.dockerRegistry.storage
- SMTP → cloud.smtp.*
4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest
#### Manifest App Reference Resolution:
When you use `{{ .apps.<ref>.* }}` in templates:
1. System checks if `<ref>` matches any dependency's `alias` field
2. If no alias match, checks if `<ref>` matches any dependency's `name` field
3. Uses the `installedAs` value (automatically added when the app is added) to find actual app configuration in `config.yaml`
All manifest template variables must be defined in one of these locations.
**Important:** In the rest of the app templates, ALL configuration keys referenced in templates (via `{{ .key }}`) must be defined in `defaultConfig`. Only the app config is available to app templates.
### Kustomization (`kustomization.yaml`) ### Kustomization (`kustomization.yaml`)
@@ -111,21 +280,7 @@ This means individual resources can use simple, component-specific selectors lik
### Gomplate Templating ### Gomplate Templating
Resource files in this repository are **templates** that get compiled when users add apps via the web app, CLI, or API. Use gomplate syntax to reference configuration: Resource files in this repository are **templates** that get compiled when users add apps via the web app, CLI, or API. Only variables defined in the manifest file's 'defaultConfig' section are available to the resource templates. Use gomplate syntax to reference configuration:
```yaml
# Common template variables
domain: {{ .cloud.domain }} # Operator's domain
email: {{ .operator.email }} # Operator's email
image: {{ .apps.myapp.serverImage }} # App-specific config
dbHost: {{ .apps.myapp.dbHostname }} # App-specific config
```
**Template variable sources:**
1. Standard Wild Cloud variables (`{{ .cloud.* }}`, `{{ .operator.* }}`)
2. App-specific variables defined in your manifest's `defaultConfig`
All template variables must be defined in one of these locations. The compiled files are placed in the instance's directory as standard Kubernetes manifests.
### External DNS ### External DNS
@@ -133,12 +288,47 @@ Ingress resources should include external-dns annotations for automatic DNS mana
```yaml ```yaml
annotations: annotations:
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }} external-dns.alpha.kubernetes.io/target: {{ .domain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
``` ```
Note: 'domain' must be defined in the app manifest's 'defaultConfig' section.
This creates a CNAME from the app subdomain to the cluster domain (e.g., `myapp.cloud.example.com``cloud.example.com`). This creates a CNAME from the app subdomain to the cluster domain (e.g., `myapp.cloud.example.com``cloud.example.com`).
## App Dependencies and Reference Mapping
### How Dependency References Work
When an app depends on other apps, the reference system allows flexibility in naming while maintaining clear relationships:
1. **Define dependencies** in your manifest with optional aliases:
```yaml
requires:
- name: postgres # Actual app to depend on
alias: db # Optional: how to reference it in templates
- name: redis # No alias means use 'redis' as reference
```
2. **At installation time**, the system:
- Prompts user to map dependencies to actual installed apps
- Sets `installedAs` field in the local app manifest to track the mapping
- Example: User might have `postgres-primary` installed, mapped to the `db` dependency
### Example: Multiple Database Instances
If a user has multiple PostgreSQL instances:
```yaml
# User's config.yaml
apps:
postgres-primary:
hostname: primary.postgres.svc.cluster.local
postgres-analytics:
hostname: analytics.postgres.svc.cluster.local
```
When adding an app that requires postgres, they can choose which instance to use, and the system tracks this in the manifest's `installedAs` field.
## Database Patterns ## Database Patterns
### Database Initialization Jobs ### Database Initialization Jobs
@@ -211,13 +401,16 @@ spec:
### Secrets Management ### Secrets Management
Secrets use a **full dotted-path naming convention** to prevent naming conflicts: Secrets are managed through two mechanisms: default secrets for the app itself and required secrets from dependencies.
**In manifest:** **In manifest:**
```yaml ```yaml
defaultSecrets: defaultSecrets:
- apps.myapp.dbPassword key: dbPassword # This app's database password
- apps.postgres.password key: apiKey # This app's API key
requiredSecrets:
- db.password # Password from postgres dependency (aliased as 'db')
- redis.auth # Auth from redis dependency
``` ```
**In resources:** **In resources:**
@@ -227,14 +420,26 @@ env:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: myapp-secrets name: myapp-secrets
key: apps.myapp.dbPassword # Full dotted path, not just "dbPassword" key: dbPassword # Points to the default secret
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db.password # Points to the required secret
``` ```
**Secret workflow:** **Secret workflow:**
1. List secrets in manifest's `defaultSecrets` 1. Define app's own secrets in `defaultSecrets` (key, default mappings)
2. When adding an app, the system generates random values in the instance's `secrets.yaml` 2. Reference dependency secrets in `requiredSecrets` (list)
3. When deploying, the system creates a Kubernetes Secret named `<app-name>-secrets` 3. When adding an app, the system:
4. Resources reference secrets using full dotted paths - Generates random values for empty `defaultSecrets`
- Copies referenced secrets from dependencies
- Stores all in the instance's `secrets.yaml`
4. When deploying, creates a Kubernetes Secret named `<app-name>-secrets` containing:
- All `defaultSecrets` with key format: `<key>`
- All `requiredSecrets` with key format: `<app-ref>.<key>`
**Key collision handling:** If the same key exists in both `defaultSecrets` and `requiredSecrets`, the `requiredSecrets` value takes precedence. Authors should ensure their local secrets don't collide with their required secrets.
**Important:** Never commit `secrets.yaml` to Git. Templates should only reference secrets, never contain actual secret values. **Important:** Never commit `secrets.yaml` to Git. Templates should only reference secrets, never contain actual secret values.
@@ -308,9 +513,11 @@ Before submitting a new or modified app, verify:
- [ ] **Manifest** - [ ] **Manifest**
- [ ] `name` matches directory name - [ ] `name` matches directory name
- [ ] All required fields present (`name`, `description`, `version`, `defaultConfig`) - [ ] All required fields present (`name`, `description`, `version`, `defaultConfig`)
- [ ] All template variables defined in `defaultConfig` or are standard Wild Cloud variables - [ ] All template variables defined in `defaultConfig`
- [ ] Secrets use dotted-path format (e.g., `apps.appname.secretname`) - [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
- [ ] Dependencies listed in `requires` (if any) - [ ] `requiredSecrets` references use `<app-ref>.<key>` format
- [ ] Dependencies listed in `requires` with optional `alias` fields
- [ ] Manifest template references match dependency aliases or names
- [ ] **Kustomization** - [ ] **Kustomization**
- [ ] Includes standard Wild Cloud labels with `includeSelectors: true` - [ ] Includes standard Wild Cloud labels with `includeSelectors: true`
@@ -318,8 +525,6 @@ Before submitting a new or modified app, verify:
- [ ] All resource files listed under `resources:` - [ ] All resource files listed under `resources:`
- [ ] **Resources** - [ ] **Resources**
- [ ] All hardcoded values replaced with gomplate variables
- [ ] Secrets reference full dotted paths
- [ ] Security contexts on all pods (both pod-level and container-level) - [ ] Security contexts on all pods (both pod-level and container-level)
- [ ] Simple component labels, no Helm-style labels - [ ] Simple component labels, no Helm-style labels
- [ ] Ingresses include external-dns annotations - [ ] Ingresses include external-dns annotations

View File

@@ -1,15 +1,97 @@
- @README.md - @README.md
- @ADDING-APPS.md - @ADDING-APPS.md
## Finding good sources of documentation for adding a new app to the Wild Cloud Directory
- A good starting point is to:
- look for an app's official documentation on running in Kubernetes or a containerized environment
- look at the app's official docker compose
- look for official or common helm packages
- look at the source code repository for the app
These sources will oftentimes not use the latest version. Check to make sure you are adding the latest app version.
- Don't use helm for the final deployement, however it is a good idea to unpack a helm package to investigate best practices and to overcome tricky configurations
## App package development lifecycle
- when developing a new app, test on the `test-cloud` instance in the `/home/payne/repos/wild-cloud-dev/wild-cloud-redmond-data` wild data dir. Prefer the `wild` CLI for managing app lifecycle as it takes care of copying and compiling kustomize templates. Example commands:
- `wild instance use test-cloud`
- `wild app add <app>`
- `wild app deploy <app>`
- `wild app delete`
- But you can always use `kubectl` directly. Just make sure you use the `test-cloud` `kubeconfig` and, when applying resources, use the `-k` flag so kustomize templates get copied.
- kubectl --kubeconfig=/home/payne/repos/wild-cloud-redmond-data/instances/test-cloud/kubeconfig get pods -n <app>
- While we test in `/home/payne/repos/wild-cloud-dev/wild-cloud-redmond-data/instances/test-cloud`, the final product (our app package) must be in `/home/payne/repos/wild-cloud-dev/wild-directory`. Always make sure that, in the end, whatever is in `wild-directory` can be deployed into `test-cloud`.
- If you run into difficulties, revisit helm charts, docker compose files, and most importantly, the source code to determine how existing deployments are functioning correctly.
## App Icons
Icon Search Process
1. Primary Source: Dashboard Icons (homarr-labs)
- URL Pattern: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/{app-name}.svg
- Why: Curated collection specifically for self-hosted apps, consistent style, reliable CDN
- Format: SVG (preferred for scalability)
- Check: Visit https://dashboardicons.com/icons/{app-name} to see if it exists
2. Fallback: Vector Logo Zone
- URL Pattern: https://www.vectorlogo.zone/logos/{app-name}/
- Why: Large collection of official logos in standardized formats
- Options:
- {app-name}-icon.svg (logo only, no text)
- {app-name}-ar21.svg (logo with text)
- Best for: Apps not in Dashboard Icons
3. Official Sources
- Check the app's official website for logo/brand pages
- Look for /brand, /logos, /assets paths
- Example: https://www.loomio.com/brand/logo_gold.svg
4. Community CDNs
- LobeHub Icons: For AI/LLM tools (vLLM, etc.)
- https://unpkg.com/@lobehub/icons-static-png@latest/dark/{app-name}.png
- Simple Icons: For popular brands
- https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/{app-name}.svg
Validation Process
For each candidate URL:
1. Test the URL using WebFetch to confirm it returns a valid image
2. Verify format: SVG preferred, PNG acceptable
3. Check content: Logo-only preferred over logo+text
4. Confirm it works: Actually loads in a browser/img tag
Icon Preferences
1. Format: SVG > PNG (for scalability)
2. Style: Logo only > Logo with text
3. Source: Official CDN > Community CDN > Direct hosting
4. Consistency: Similar visual style across all apps
Search Strategy
### For each app:
1. Try Dashboard Icons first: dashboardicons.com/icons/{app}
2. If not found, try Vector Logo Zone: vectorlogo.zone/logos/{app}
3. If still not found, search: "{app} official logo CDN SVG URL"
4. Validate the URL actually works
5. Prefer icon-only versions when multiple options exist
This systematic approach ensures consistent, reliable, and high-quality icons across all Wild Cloud apps.
## IMPORTANT ## IMPORTANT
- NEVER under any circumstances work on any Wild Cloud instance other than `test-cloud`
- `secrets.yaml` is NOT checked in and any values unrelated to your current task should be preserved
- When adding a new app to the directory, check to make sure you are adding the latest app version. - When adding a new app to the directory, check to make sure you are adding the latest app version.
- set `namespace` in default config and refer to it using `{{ .namespace }}` in the `namespace.yaml` definition file and the `kustomization.yaml` file.
- If the app requires a specific platform (amd64, arm64, etc.), make sure it is called out in the manifest and the k8s tags are set
- Use traefik for ingress. - Use traefik for ingress.
- Use postgres for database if supported. - Use postgres for database if supported.
- Keep config key naming (including nesting) consistent with other apps. - Keep config key naming (including nesting) consistent with other apps.
- Don't use helm
- If the app requires a specific platform (amd64, arm64, etc.), make sure it is called out in the manifest and the k8s tags are set
- when developing a new app:
- test with:
- reset to a fresh state between tests:
- secrets.yaml is not checked in and any values unrelated to your current task should be preserved

View File

@@ -97,20 +97,6 @@ Some apps require other apps to function. For example:
When you add an app, check its `requires` field in the manifest and ensure dependencies are added first. When you add an app, check its `requires` field in the manifest and ensure dependencies are added first.
## Available Apps
This repository includes apps for:
- Content management (Ghost, Discourse)
- Project management (OpenProject)
- Photo management (Immich)
- Code hosting (Gitea)
- Email marketing (Listmonk, Keila)
- AI interfaces (Open WebUI, vLLM)
- Databases (PostgreSQL, MySQL, Redis, Memcached)
- And more...
Browse the full catalog with descriptions through the web app, CLI, or via the API endpoint `/api/v1/apps/available`.
## Contributing ## Contributing
Want to add a new app or improve an existing one? See [ADDING-APPS.md](ADDING-APPS.md) for detailed guidance on creating Wild Cloud apps. Want to add a new app or improve an existing one? See [ADDING-APPS.md](ADDING-APPS.md) for detailed guidance on creating Wild Cloud apps.

25
decidim/Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# Build Decidim with Sidekiq support
FROM decidim/decidim:0.31.0
# Switch to root to install dependencies
USER root
# Add sidekiq to Gemfile
RUN cd /code && \
echo "" >> Gemfile && \
echo "# Background job processing" >> Gemfile && \
echo "gem 'sidekiq', '~> 6.5'" >> Gemfile && \
bundle install
# Configure Rails to use Sidekiq as ActiveJob backend
RUN cd /code && \
test -d config/initializers || mkdir -p config/initializers && \
echo "Rails.application.config.active_job.queue_adapter = :sidekiq" > config/initializers/active_job.rb && \
cat config/initializers/active_job.rb && \
ls -la config/initializers/
# Switch back to decidim user
USER decidim
# Default command (can be overridden)
CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]

73
decidim/db-init-job.yaml Normal file
View File

@@ -0,0 +1,73 @@
---
apiVersion: batch/v1
kind: Job
metadata:
name: decidim-db-init
namespace: decidim
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: db-init
image: postgres:17
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
command:
- /bin/bash
- -c
- |
set -e
export PGPASSWORD="${POSTGRES_ADMIN_PASSWORD}"
# Create database if it doesn't exist
psql -h "${POSTGRES_HOST}" -U "${POSTGRES_ADMIN_USER}" -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = '${DB_NAME}'" | grep -q 1 || \
psql -h "${POSTGRES_HOST}" -U "${POSTGRES_ADMIN_USER}" -d postgres -c "CREATE DATABASE ${DB_NAME};"
# Create user if it doesn't exist, or update password if it does
psql -h "${POSTGRES_HOST}" -U "${POSTGRES_ADMIN_USER}" -d postgres -tc "SELECT 1 FROM pg_roles WHERE rolname = '${DB_USER}'" | grep -q 1 && \
psql -h "${POSTGRES_HOST}" -U "${POSTGRES_ADMIN_USER}" -d postgres -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" || \
psql -h "${POSTGRES_HOST}" -U "${POSTGRES_ADMIN_USER}" -d postgres -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';"
# Grant privileges
psql -h "${POSTGRES_HOST}" -U "${POSTGRES_ADMIN_USER}" -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
# Grant schema privileges (needed for Rails migrations)
psql -h "${POSTGRES_HOST}" -U "${POSTGRES_ADMIN_USER}" -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
echo "Database initialization completed successfully"
env:
- name: POSTGRES_HOST
value: {{ .dbHostname }}
- name: POSTGRES_ADMIN_USER
value: postgres
- name: POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: decidim-secrets
key: postgres.password
- name: DB_NAME
value: {{ .dbName }}
- name: DB_USER
value: {{ .dbUsername }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: decidim-secrets
key: dbPassword

214
decidim/deployment.yaml Normal file
View File

@@ -0,0 +1,214 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: decidim
namespace: decidim
spec:
replicas: 1
selector:
matchLabels:
component: web
strategy:
type: Recreate
template:
metadata:
labels:
component: web
spec:
automountServiceAccountToken: false
serviceAccountName: decidim
securityContext:
fsGroup: 1000
fsGroupChangePolicy: Always
containers:
- name: decidim
image: payneio/decidim-sidekiq:0.31.0
imagePullPolicy: Always
command:
- /bin/bash
- -c
- |
set -e
cd /code
bundle exec rake db:migrate
bundle exec rails runner "Decidim::System::Admin.find_or_create_by!(email: ENV['SYSTEM_ADMIN_EMAIL']) { |admin| admin.password = ENV['SYSTEM_ADMIN_PASSWORD']; admin.password_confirmation = ENV['SYSTEM_ADMIN_PASSWORD'] }"
bundle exec rails s -b 0.0.0.0
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
add:
- CHOWN
- FOWNER
- SETGID
- SETUID
- DAC_OVERRIDE
privileged: false
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
env:
- name: RAILS_ENV
value: "production"
- name: PORT
value: "{{ .port }}"
- name: RAILS_LOG_TO_STDOUT
value: "true"
# Database configuration
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: decidim-secrets
key: dbUrl
# Redis configuration
- name: REDIS_HOSTNAME
value: {{ .redisHostname }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: decidim-secrets
key: redis.password
- name: REDIS_URL
value: "redis://:$(REDIS_PASSWORD)@$(REDIS_HOSTNAME):6379/0"
# Application configuration
- name: DECIDIM_HOST
value: {{ .domain }}
- name: DECIDIM_ORGANIZATION_NAME
value: {{ .siteName }}
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: decidim-secrets
key: secretKeyBase
# SMTP configuration
- name: SMTP_ADDRESS
value: {{ .smtp.host }}
- name: SMTP_PORT
value: "{{ .smtp.port }}"
- name: SMTP_USERNAME
value: {{ .smtp.user }}
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: decidim-secrets
key: smtpPassword
- name: SMTP_DOMAIN
value: {{ .domain }}
- name: SMTP_FROM
value: {{ .smtp.from }}
- name: SMTP_STARTTLS_AUTO
value: "{{ .smtp.startTls }}"
# System admin credentials
- name: SYSTEM_ADMIN_EMAIL
value: {{ .systemAdminEmail }}
- name: SYSTEM_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: decidim-secrets
key: systemAdminPassword
ports:
- name: http
containerPort: {{ .port }}
protocol: TCP
livenessProbe:
tcpSocket:
port: {{ .port }}
initialDelaySeconds: 300
periodSeconds: 30
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 6
readinessProbe:
tcpSocket:
port: {{ .port }}
initialDelaySeconds: 180
periodSeconds: 30
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 6
resources:
limits:
cpu: 2000m
ephemeral-storage: 10Gi
memory: 4Gi
requests:
cpu: 500m
ephemeral-storage: 50Mi
memory: 1Gi
volumeMounts:
- name: decidim-data
mountPath: /code/public/uploads
- name: sidekiq
image: payneio/decidim-sidekiq:0.31.0
imagePullPolicy: Always
command:
- /bin/bash
- -c
- |
set -e
cd /code
bundle exec sidekiq
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
add:
- CHOWN
- FOWNER
- SETGID
- SETUID
- DAC_OVERRIDE
privileged: false
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
env:
- name: RAILS_ENV
value: "production"
- name: RAILS_LOG_TO_STDOUT
value: "true"
# Database configuration
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: decidim-secrets
key: dbUrl
# Redis configuration
- name: REDIS_HOSTNAME
value: {{ .redisHostname }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: decidim-secrets
key: redis.password
- name: REDIS_URL
value: "redis://:$(REDIS_PASSWORD)@$(REDIS_HOSTNAME):6379/0"
# Application configuration
- name: DECIDIM_HOST
value: {{ .domain }}
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: decidim-secrets
key: secretKeyBase
resources:
limits:
cpu: 1000m
memory: 2Gi
requests:
cpu: 250m
memory: 512Mi
volumeMounts:
- name: decidim-data
mountPath: /code/public/uploads
volumes:
- name: decidim-data
persistentVolumeClaim:
claimName: decidim-data

26
decidim/ingress.yaml Normal file
View File

@@ -0,0 +1,26 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: decidim
namespace: decidim
annotations:
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}
rules:
- host: {{ .domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: decidim
port:
number: {{ .port }}

View File

@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: decidim
labels:
- includeSelectors: true
pairs:
app: decidim
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- serviceaccount.yaml
- pvc.yaml
- db-init-job.yaml
- deployment.yaml
- service.yaml
- ingress.yaml

44
decidim/manifest.yaml Normal file
View File

@@ -0,0 +1,44 @@
name: decidim
is: decidim
description: Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
version: 0.31.0
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
requires:
- name: postgres
installed_as: postgres
- name: redis
installed_as: redis
defaultConfig:
namespace: decidim
externalDnsDomain: "{{ .cloud.domain }}"
timezone: UTC
port: 3000
storage: 20Gi
systemAdminEmail: "{{ .operator.email }}"
siteName: "Decidim"
domain: decidim.{{ .cloud.domain }}
dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbUsername: decidim
dbName: decidim
redisHostname: "{{ .apps.redis.host }}"
tlsSecretName: wildcard-wild-cloud-tls
smtp:
enabled: true
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
from: "{{ .cloud.smtp.from }}"
tls: "{{ .cloud.smtp.tls }}"
startTls: "{{ .cloud.smtp.startTls }}"
defaultSecrets:
- key: systemAdminPassword
- key: secretKeyBase
default: "{{ random.AlphaNum 128 }}"
- key: smtpPassword
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}"
requiredSecrets:
- postgres.password
- redis.password

4
decidim/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: decidim

12
decidim/pvc.yaml Normal file
View File

@@ -0,0 +1,12 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: decidim-data
namespace: decidim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .storage }}

15
decidim/service.yaml Normal file
View File

@@ -0,0 +1,15 @@
---
apiVersion: v1
kind: Service
metadata:
name: decidim
namespace: decidim
spec:
selector:
component: web
ports:
- name: http
port: {{ .port }}
targetPort: http
protocol: TCP
type: ClusterIP

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: decidim
namespace: decidim
automountServiceAccountToken: false

View File

@@ -1,31 +0,0 @@
---
# Source: discourse/templates/configmaps.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: discourse
namespace: discourse
data:
DISCOURSE_HOSTNAME: "{{ .apps.discourse.domain }}"
DISCOURSE_SKIP_INSTALL: "no"
DISCOURSE_SITE_NAME: "{{ .apps.discourse.siteName }}"
DISCOURSE_USERNAME: "{{ .apps.discourse.adminUsername }}"
DISCOURSE_EMAIL: "{{ .apps.discourse.adminEmail }}"
DISCOURSE_REDIS_HOST: "{{ .apps.discourse.redisHostname }}"
DISCOURSE_REDIS_PORT_NUMBER: "6379"
DISCOURSE_DATABASE_HOST: "{{ .apps.discourse.dbHostname }}"
DISCOURSE_DATABASE_PORT_NUMBER: "5432"
DISCOURSE_DATABASE_NAME: "{{ .apps.discourse.dbName }}"
DISCOURSE_DATABASE_USER: "{{ .apps.discourse.dbUsername }}"
DISCOURSE_SMTP_HOST: "{{ .apps.discourse.smtp.host }}"
DISCOURSE_SMTP_PORT: "{{ .apps.discourse.smtp.port }}"
DISCOURSE_SMTP_USER: "{{ .apps.discourse.smtp.user }}"
DISCOURSE_SMTP_PROTOCOL: "tls"
DISCOURSE_SMTP_AUTH: "login"
# DISCOURSE_PRECOMPILE_ASSETS: "false"
# DISCOURSE_SKIP_INSTALL: "no"
# DISCOURSE_SKIP_BOOTSTRAP: "yes"

View File

@@ -27,7 +27,7 @@ spec:
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
env: env:
- name: PGHOST - name: PGHOST
value: "{{ .apps.discourse.dbHostname }}" value: "{{ .dbHostname }}"
- name: PGPORT - name: PGPORT
value: "5432" value: "5432"
- name: PGUSER - name: PGUSER
@@ -36,16 +36,16 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.postgres.password key: postgres.password
- name: DISCOURSE_DB_USER - name: DISCOURSE_DB_USER
value: "{{ .apps.discourse.dbUsername }}" value: "{{ .dbUsername }}"
- name: DISCOURSE_DB_NAME - name: DISCOURSE_DB_NAME
value: "{{ .apps.discourse.dbName }}" value: "{{ .dbName }}"
- name: DISCOURSE_DB_PASSWORD - name: DISCOURSE_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.discourse.dbPassword key: dbPassword
command: command:
- /bin/sh - /bin/sh
- -c - -c

View File

@@ -1,5 +1,4 @@
--- ---
# Source: discourse/templates/deployment.yaml
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -18,219 +17,288 @@ spec:
component: web component: web
spec: spec:
automountServiceAccountToken: false automountServiceAccountToken: false
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
component: web
topologyKey: kubernetes.io/hostname
weight: 1
serviceAccountName: discourse serviceAccountName: discourse
securityContext: securityContext:
fsGroup: 0 fsGroup: 1000
fsGroupChangePolicy: Always fsGroupChangePolicy: Always
supplementalGroups: []
sysctls: []
initContainers: initContainers:
containers: - name: discourse-migrate
- name: discourse image: discourse/discourse:3.5.3
image: docker.io/bitnami/discourse:3.4.7-debian-12-r0
imagePullPolicy: "IfNotPresent" imagePullPolicy: "IfNotPresent"
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop:
- ALL
add: add:
- CHOWN - CHOWN
- SYS_CHROOT
- FOWNER - FOWNER
- SETGID - SETGID
- SETUID - SETUID
- DAC_OVERRIDE - DAC_OVERRIDE
drop:
- ALL
privileged: false privileged: false
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
runAsGroup: 0
runAsNonRoot: false runAsNonRoot: false
runAsUser: 0 runAsUser: 0
seLinuxOptions: {}
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
command:
- /bin/bash
- -c
- |
set -e
cd /var/www/discourse
export HOME=/root
git config --global --add safe.directory /var/www/discourse
bundle exec rake db:migrate
bundle exec rake assets:precompile
env: env:
- name: BITNAMI_DEBUG - name: RAILS_ENV
value: "false" value: "production"
- name: DISCOURSE_PASSWORD - name: DISCOURSE_DB_HOST
value: {{ .dbHostname }}
- name: DISCOURSE_DB_PORT
value: "{{ .dbPort }}"
- name: DISCOURSE_DB_NAME
value: {{ .dbName }}
- name: DISCOURSE_DB_USERNAME
value: {{ .dbUsername }}
- name: DISCOURSE_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.discourse.adminPassword key: dbPassword
- name: DISCOURSE_PORT_NUMBER - name: DISCOURSE_REDIS_HOST
value: "8080" value: {{ .redisHostname }}
- name: DISCOURSE_EXTERNAL_HTTP_PORT_NUMBER
value: "80"
- name: DISCOURSE_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.dbPassword
- name: POSTGRESQL_CLIENT_CREATE_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.dbPassword
- name: DISCOURSE_REDIS_PASSWORD - name: DISCOURSE_REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.redis.password key: redis.password
- name: DISCOURSE_HOSTNAME
value: {{ .domain }}
- name: DISCOURSE_SECRET_KEY_BASE - name: DISCOURSE_SECRET_KEY_BASE
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.discourse.secretKeyBase key: secretKeyBase
volumeMounts:
- name: discourse-data
mountPath: /shared
containers:
- name: discourse
image: discourse/discourse:3.5.3
imagePullPolicy: "IfNotPresent"
command:
- /sbin/boot
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
add:
- CHOWN
- FOWNER
- SETGID
- SETUID
- DAC_OVERRIDE
privileged: false
readOnlyRootFilesystem: false
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault
env:
- name: RAILS_ENV
value: "production"
# Discourse database configuration
- name: DISCOURSE_DB_HOST
value: {{ .dbHostname }}
- name: DISCOURSE_DB_PORT
value: "{{ .dbPort }}"
- name: DISCOURSE_DB_NAME
value: {{ .dbName }}
- name: DISCOURSE_DB_USERNAME
value: {{ .dbUsername }}
- name: DISCOURSE_DB_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: dbPassword
# Redis configuration
- name: DISCOURSE_REDIS_HOST
value: {{ .redisHostname }}
- name: DISCOURSE_REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: redis.password
# Site configuration
- name: DISCOURSE_HOSTNAME
value: {{ .domain }}
- name: DISCOURSE_DEVELOPER_EMAILS
value: {{ .adminEmail }}
- name: DISCOURSE_SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: discourse-secrets
key: secretKeyBase
# SMTP configuration
- name: DISCOURSE_SMTP_ADDRESS
value: {{ .smtp.host }}
- name: DISCOURSE_SMTP_PORT
value: "{{ .smtp.port }}"
- name: DISCOURSE_SMTP_USER_NAME
value: {{ .smtp.user }}
- name: DISCOURSE_SMTP_PASSWORD - name: DISCOURSE_SMTP_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.discourse.smtpPassword key: smtpPassword
- name: SMTP_PASSWORD - name: DISCOURSE_SMTP_ENABLE_START_TLS
valueFrom: value: "{{ .smtp.startTls }}"
secretKeyRef:
name: discourse-secrets
key: apps.discourse.smtpPassword
envFrom:
- configMapRef:
name: discourse
ports: ports:
- name: http - name: http
containerPort: 8080 containerPort: 80
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
tcpSocket: httpGet:
path: /srv/status
port: http port: http
initialDelaySeconds: 500 initialDelaySeconds: 500
periodSeconds: 10 periodSeconds: 30
timeoutSeconds: 5 timeoutSeconds: 10
successThreshold: 1 successThreshold: 1
failureThreshold: 6 failureThreshold: 6
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /srv/status path: /srv/status
port: http port: http
initialDelaySeconds: 180 initialDelaySeconds: 360
periodSeconds: 10 periodSeconds: 30
timeoutSeconds: 5 timeoutSeconds: 10
successThreshold: 1 successThreshold: 1
failureThreshold: 6 failureThreshold: 6
resources: resources:
limits: limits:
cpu: 1 cpu: 2000m
ephemeral-storage: 2Gi ephemeral-storage: 10Gi
memory: 8Gi # for precompiling assets! memory: 8Gi
requests: requests:
cpu: 750m cpu: 750m
ephemeral-storage: 50Mi ephemeral-storage: 50Mi
memory: 1Gi memory: 1Gi
volumeMounts: volumeMounts:
- name: discourse-data - name: discourse-data
mountPath: /bitnami/discourse mountPath: /shared
subPath: discourse
- name: sidekiq - name: sidekiq
image: docker.io/bitnami/discourse:3.4.7-debian-12-r0 image: discourse/discourse:3.5.3
imagePullPolicy: "IfNotPresent" imagePullPolicy: "IfNotPresent"
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop:
- ALL
add: add:
- CHOWN - CHOWN
- SYS_CHROOT
- FOWNER - FOWNER
- SETGID - SETGID
- SETUID - SETUID
- DAC_OVERRIDE - DAC_OVERRIDE
drop:
- ALL
privileged: false privileged: false
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
runAsGroup: 0
runAsNonRoot: false runAsNonRoot: false
runAsUser: 0 runAsUser: 0
seLinuxOptions: {}
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
command: command:
- /opt/bitnami/scripts/discourse/entrypoint.sh - /bin/bash
args: - -c
- /opt/bitnami/scripts/discourse-sidekiq/run.sh - "cd /var/www/discourse && export HOME=/root && exec bundle exec sidekiq"
env: env:
- name: BITNAMI_DEBUG - name: RAILS_ENV
value: "false" value: "production"
- name: DISCOURSE_PASSWORD # Discourse database configuration
- name: DISCOURSE_DB_HOST
value: {{ .dbHostname }}
- name: DISCOURSE_DB_PORT
value: "{{ .dbPort }}"
- name: DISCOURSE_DB_NAME
value: {{ .dbName }}
- name: DISCOURSE_DB_USERNAME
value: {{ .dbUsername }}
- name: DISCOURSE_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.discourse.adminPassword key: dbPassword
- name: DISCOURSE_POSTGRESQL_PASSWORD # Redis configuration
valueFrom: - name: DISCOURSE_REDIS_HOST
secretKeyRef: value: {{ .redisHostname }}
name: discourse-secrets
key: apps.discourse.dbPassword
- name: DISCOURSE_REDIS_PASSWORD - name: DISCOURSE_REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.redis.password key: redis.password
# Site configuration
- name: DISCOURSE_HOSTNAME
value: {{ .domain }}
- name: DISCOURSE_DEVELOPER_EMAILS
value: {{ .adminEmail }}
- name: DISCOURSE_SECRET_KEY_BASE - name: DISCOURSE_SECRET_KEY_BASE
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.discourse.secretKeyBase key: secretKeyBase
# SMTP configuration
- name: DISCOURSE_SMTP_ADDRESS
value: {{ .smtp.host }}
- name: DISCOURSE_SMTP_PORT
value: "{{ .smtp.port }}"
- name: DISCOURSE_SMTP_USER_NAME
value: {{ .smtp.user }}
- name: DISCOURSE_SMTP_PASSWORD - name: DISCOURSE_SMTP_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: apps.discourse.smtpPassword key: smtpPassword
- name: SMTP_PASSWORD - name: DISCOURSE_SMTP_ENABLE_START_TLS
valueFrom: value: "{{ .smtp.startTls }}"
secretKeyRef:
name: discourse-secrets
key: apps.discourse.smtpPassword
envFrom:
- configMapRef:
name: discourse
livenessProbe: livenessProbe:
exec: exec:
command: ["/bin/sh", "-c", "pgrep -f ^sidekiq"] command:
- /bin/bash
- -c
- "pgrep -f sidekiq"
initialDelaySeconds: 500 initialDelaySeconds: 500
periodSeconds: 10 periodSeconds: 30
timeoutSeconds: 5 timeoutSeconds: 10
successThreshold: 1 successThreshold: 1
failureThreshold: 6 failureThreshold: 6
readinessProbe: readinessProbe:
exec: exec:
command: ["/bin/sh", "-c", "pgrep -f ^sidekiq"] command:
initialDelaySeconds: 30 - /bin/bash
periodSeconds: 10 - -c
timeoutSeconds: 5 - "pgrep -f sidekiq"
initialDelaySeconds: 180
periodSeconds: 30
timeoutSeconds: 10
successThreshold: 1 successThreshold: 1
failureThreshold: 6 failureThreshold: 6
resources: resources:
limits: limits:
cpu: 500m cpu: 1000m
ephemeral-storage: 2Gi ephemeral-storage: 2Gi
memory: 768Mi memory: 1Gi
requests: requests:
cpu: 375m cpu: 375m
ephemeral-storage: 50Mi ephemeral-storage: 50Mi
memory: 512Mi memory: 512Mi
volumeMounts: volumeMounts:
- name: discourse-data - name: discourse-data
mountPath: /bitnami/discourse mountPath: /shared
subPath: discourse
volumes: volumes:
- name: discourse-data - name: discourse-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: discourse claimName: discourse-data

View File

@@ -4,13 +4,13 @@ apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: discourse name: discourse
namespace: "discourse" namespace: "{{ .namespace }}"
annotations: annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: "{{ .cloud.domain }}" external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
spec: spec:
rules: rules:
- host: "{{ .apps.discourse.domain }}" - host: "{{ .domain }}"
http: http:
paths: paths:
- path: / - path: /
@@ -22,5 +22,5 @@ spec:
name: http name: http
tls: tls:
- hosts: - hosts:
- "{{ .apps.discourse.domain }}" - "{{ .domain }}"
secretName: wildcard-external-wild-cloud-tls secretName: wildcard-external-wild-cloud-tls

View File

@@ -10,7 +10,6 @@ labels:
resources: resources:
- namespace.yaml - namespace.yaml
- serviceaccount.yaml - serviceaccount.yaml
- configmap.yaml
- pvc.yaml - pvc.yaml
- deployment.yaml - deployment.yaml
- service.yaml - service.yaml

View File

@@ -1,22 +1,26 @@
name: discourse name: discourse
is: discourse
description: Discourse is a modern, open-source discussion platform designed for online communities and forums. description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
version: 3.4.7 version: 3.5.3
icon: https://www.discourse.org/img/icon.png icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
requires: requires:
- name: postgres - name: postgres
- name: redis - name: redis
defaultConfig: defaultConfig:
namespace: discourse
externalDnsDomain: "{{ .cloud.domain }}"
timezone: UTC timezone: UTC
port: 8080 port: 3000
storage: 10Gi storage: 10Gi
adminEmail: admin@{{ .cloud.domain }} adminEmail: "{{ .operator.email }}"
adminUsername: admin adminUsername: admin
siteName: "Community" siteName: "Community"
domain: discourse.{{ .cloud.domain }} domain: discourse.{{ .cloud.domain }}
dbHostname: postgres.postgres.svc.cluster.local dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbUsername: discourse dbUsername: discourse
dbName: discourse dbName: discourse
redisHostname: redis.redis.svc.cluster.local redisHostname: "{{ .apps.redis.host }}"
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
smtp: smtp:
enabled: false enabled: false
@@ -24,13 +28,16 @@ defaultConfig:
port: "{{ .cloud.smtp.port }}" port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}" user: "{{ .cloud.smtp.user }}"
from: "{{ .cloud.smtp.from }}" from: "{{ .cloud.smtp.from }}"
tls: {{ .cloud.smtp.tls }} tls: "{{ .cloud.smtp.tls }}"
startTls: {{ .cloud.smtp.startTls }} startTls: "{{ .cloud.smtp.startTls }}"
defaultSecrets: defaultSecrets:
- - key: apps.discourse.adminPassword - key: adminPassword
- - key: apps.discourse.dbPassword - key: secretKeyBase
- - key: apps.discourse.dbUrl default: "{{ random.AlphaNum 64 }}"
- - key: apps.redis.password - key: smtpPassword
- - key: apps.discourse.secretKeyBase - key: dbPassword
- - key: apps.discourse.smtpPassword - key: dbUrl
- - key: apps.postgres.password default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode=disable"
requiredSecrets:
- postgres.password
- redis.password

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: discourse name: "{{ .namespace }}"

View File

@@ -1,13 +1,13 @@
--- ---
# Source: discourse/templates/pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim
metadata: metadata:
name: discourse name: discourse-data
namespace: discourse namespace: discourse
spec: spec:
accessModes: accessModes:
- "ReadWriteOnce" - ReadWriteOnce
resources: resources:
requests: requests:
storage: "{{ .apps.discourse.storage }}" storage: {{ .storage }}
storageClassName: longhorn

View File

@@ -3,7 +3,6 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: example-admin name: example-admin
namespace: example-admin
labels: labels:
app: example-admin app: example-admin
spec: spec:

View File

@@ -3,10 +3,9 @@ apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: example-admin name: example-admin
namespace: example-admin
spec: spec:
rules: rules:
- host: example-admin.{{ .cloud.internalDomain }} - host: "{{ .host }}"
http: http:
paths: paths:
- path: / - path: /
@@ -18,5 +17,5 @@ spec:
number: 80 number: 80
tls: tls:
- hosts: - hosts:
- example-admin.{{ .cloud.internalDomain }} - "{{ .host }}"
secretName: wildcard-internal-wild-cloud-tls secretName: wildcard-internal-wild-cloud-tls

View File

@@ -1,6 +1,10 @@
name: example-admin name: example-admin
is: example
install: true install: true
description: An example application that is deployed with internal-only access. description: An example application that is deployed with internal-only access.
version: 1.0.0 version: 1.0.0
defaultConfig: defaultConfig:
namespace: example-admin
externalDnsDomain: '{{ .cloud.domain }}'
host: '{{ .host }}'
tlsSecretName: wildcard-internal-wild-cloud-tls tlsSecretName: wildcard-internal-wild-cloud-tls

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: example-admin name: "{{ .namespace }}"

View File

@@ -4,7 +4,7 @@ kind: Ingress
metadata: metadata:
name: example-app name: example-app
annotations: annotations:
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }} external-dns.alpha.kubernetes.io/target: "{{ .cloud.externalDnsTarget }}"
external-dns.alpha.kubernetes.io/cloudflare-proxied: false external-dns.alpha.kubernetes.io/cloudflare-proxied: false
# Optional: Enable HTTPS redirection # Optional: Enable HTTPS redirection
@@ -15,7 +15,7 @@ metadata:
# traefik.ingress.kubernetes.io/auth-secret: basic-auth # traefik.ingress.kubernetes.io/auth-secret: basic-auth
spec: spec:
rules: rules:
- host: example-app.{{ .cloud.domain }} - host: "{{ .host }}"
http: http:
paths: paths:
- path: / - path: /
@@ -27,5 +27,5 @@ spec:
number: 80 number: 80
tls: tls:
- hosts: - hosts:
- example-app.{{ .cloud.domain }} - "{{ .host }}"
secretName: wildcard-wild-cloud-tls secretName: wildcard-wild-cloud-tls

View File

@@ -1,6 +1,10 @@
name: example-app name: example-app
is: example
install: true install: true
description: An example application that is deployed with public access. description: An example application that is deployed with public access.
version: 1.0.0 version: 1.0.0
defaultConfig: defaultConfig:
namespace: example-app
externalDnsDomain: '{{ .cloud.domain }}'
host: example-app.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: example-app name: "{{ .namespace }}"

View File

@@ -12,7 +12,7 @@ spec:
spec: spec:
containers: containers:
- name: db-init - name: db-init
image: {{ .apps.mysql.image }} image: mysql:9.1.0
command: ["/bin/bash", "-c"] command: ["/bin/bash", "-c"]
args: args:
- | - |
@@ -27,18 +27,18 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: mysql-secrets name: mysql-secrets
key: apps.mysql.rootPassword key: rootPassword
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.ghost.dbHost }}" value: "{{ .dbHost }}"
- name: DB_PORT - name: DB_PORT
value: "{{ .apps.ghost.dbPort }}" value: "{{ .dbPort }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
value: "{{ .apps.ghost.dbName }}" value: "{{ .dbName }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .apps.ghost.dbUser }}" value: "{{ .dbUser }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: apps.ghost.dbPassword key: dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

@@ -17,10 +17,10 @@ spec:
spec: spec:
containers: containers:
- name: ghost - name: ghost
image: {{ .apps.ghost.image }} image: {{ .image }}
ports: ports:
- name: http - name: http
containerPort: {{ .apps.ghost.port }} containerPort: {{ .port }}
protocol: TCP protocol: TCP
env: env:
- name: BITNAMI_DEBUG - name: BITNAMI_DEBUG
@@ -28,33 +28,33 @@ spec:
- name: ALLOW_EMPTY_PASSWORD - name: ALLOW_EMPTY_PASSWORD
value: "yes" value: "yes"
- name: GHOST_DATABASE_HOST - name: GHOST_DATABASE_HOST
value: {{ .apps.ghost.dbHost }} value: {{ .dbHost }}
- name: GHOST_DATABASE_PORT_NUMBER - name: GHOST_DATABASE_PORT_NUMBER
value: "{{ .apps.ghost.dbPort }}" value: "{{ .dbPort }}"
- name: GHOST_DATABASE_NAME - name: GHOST_DATABASE_NAME
value: {{ .apps.ghost.dbName }} value: {{ .dbName }}
- name: GHOST_DATABASE_USER - name: GHOST_DATABASE_USER
value: {{ .apps.ghost.dbUser }} value: {{ .dbUser }}
- name: GHOST_DATABASE_PASSWORD - name: GHOST_DATABASE_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: apps.ghost.dbPassword key: dbPassword
- name: GHOST_HOST - name: GHOST_HOST
value: {{ .apps.ghost.domain }} value: {{ .domain }}
- name: GHOST_PORT_NUMBER - name: GHOST_PORT_NUMBER
value: "{{ .apps.ghost.port }}" value: "{{ .port }}"
- name: GHOST_USERNAME - name: GHOST_USERNAME
value: {{ .apps.ghost.adminUser }} value: {{ .adminUser }}
- name: GHOST_PASSWORD - name: GHOST_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: apps.ghost.adminPassword key: adminPassword
- name: GHOST_EMAIL - name: GHOST_EMAIL
value: {{ .apps.ghost.adminEmail }} value: {{ .adminEmail }}
- name: GHOST_BLOG_TITLE - name: GHOST_BLOG_TITLE
value: {{ .apps.ghost.blogTitle }} value: {{ .blogTitle }}
- name: GHOST_ENABLE_HTTPS - name: GHOST_ENABLE_HTTPS
value: "yes" value: "yes"
- name: GHOST_EXTERNAL_HTTP_PORT_NUMBER - name: GHOST_EXTERNAL_HTTP_PORT_NUMBER
@@ -66,18 +66,18 @@ spec:
- name: GHOST_SMTP_SERVICE - name: GHOST_SMTP_SERVICE
value: SMTP value: SMTP
- name: GHOST_SMTP_HOST - name: GHOST_SMTP_HOST
value: {{ .apps.ghost.smtp.host }} value: {{ .smtp.host }}
- name: GHOST_SMTP_PORT - name: GHOST_SMTP_PORT
value: "{{ .apps.ghost.smtp.port }}" value: "{{ .smtp.port }}"
- name: GHOST_SMTP_USER - name: GHOST_SMTP_USER
value: {{ .apps.ghost.smtp.user }} value: {{ .smtp.user }}
- name: GHOST_SMTP_PASSWORD - name: GHOST_SMTP_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ghost-secrets name: ghost-secrets
key: apps.ghost.smtpPassword key: smtpPassword
- name: GHOST_SMTP_FROM_ADDRESS - name: GHOST_SMTP_FROM_ADDRESS
value: {{ .apps.ghost.smtp.from }} value: {{ .smtp.from }}
resources: resources:
limits: limits:
cpu: 375m cpu: 375m
@@ -92,7 +92,7 @@ spec:
mountPath: /bitnami/ghost mountPath: /bitnami/ghost
livenessProbe: livenessProbe:
tcpSocket: tcpSocket:
port: {{ .apps.ghost.port }} port: {{ .port }}
initialDelaySeconds: 120 initialDelaySeconds: 120
timeoutSeconds: 5 timeoutSeconds: 5
periodSeconds: 10 periodSeconds: 10

View File

@@ -7,12 +7,12 @@ metadata:
kubernetes.io/ingress.class: "traefik" kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: "letsencrypt-prod" cert-manager.io/cluster-issuer: "letsencrypt-prod"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }} external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/ttl: "60" external-dns.alpha.kubernetes.io/ttl: "60"
traefik.ingress.kubernetes.io/redirect-entry-point: https traefik.ingress.kubernetes.io/redirect-entry-point: https
spec: spec:
rules: rules:
- host: {{ .apps.ghost.domain }} - host: {{ .domain }}
http: http:
paths: paths:
- path: / - path: /
@@ -24,5 +24,5 @@ spec:
number: 80 number: 80
tls: tls:
- hosts: - hosts:
- {{ .apps.ghost.domain }} - {{ .domain }}
secretName: {{ .apps.ghost.tlsSecretName }} secretName: {{ .tlsSecretName }}

View File

@@ -1,10 +1,14 @@
name: ghost name: ghost
description: Ghost is a powerful app for new-media creators to publish, share, and grow a business around their content. is: ghost
description: Ghost is a powerful app for new-media creators to publish, share, and
grow a business around their content.
version: 5.118.1 version: 5.118.1
icon: https://ghost.org/images/logos/ghost-logo-orb.png icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
requires: requires:
- name: mysql - name: mysql
defaultConfig: defaultConfig:
namespace: ghost
externalDnsDomain: '{{ .cloud.domain }}'
image: docker.io/bitnami/ghost:5.118.1-debian-12-r0 image: docker.io/bitnami/ghost:5.118.1-debian-12-r0
domain: ghost.{{ .cloud.domain }} domain: ghost.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
@@ -15,16 +19,17 @@ defaultConfig:
dbName: ghost dbName: ghost
dbUser: ghost dbUser: ghost
adminUser: admin adminUser: admin
adminEmail: "admin@{{ .cloud.domain }}" adminEmail: {{ .operator.email }}
blogTitle: "My Blog" blogTitle: My Blog
timezone: UTC timezone: UTC
tlsSecretName: wildcard-wild-cloud-tls
smtp: smtp:
host: "{{ .cloud.smtp.host }}" host: '{{ .cloud.smtp.host }}'
port: "{{ .cloud.smtp.port }}" port: '{{ .cloud.smtp.port }}'
from: "{{ .cloud.smtp.from }}" from: '{{ .cloud.smtp.from }}'
user: "{{ .cloud.smtp.user }}" user: '{{ .cloud.smtp.user }}'
defaultSecrets: defaultSecrets:
- key: apps.ghost.adminPassword - key: adminPassword
- key: apps.ghost.dbPassword - key: dbPassword
- key: apps.ghost.smtpPassword - key: smtpPassword
requiredSecrets:
- mysql.rootPassword

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: ghost name: "{{ .namespace }}"

View File

@@ -8,4 +8,4 @@ spec:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: {{ .apps.ghost.storage }} storage: {{ .storage }}

View File

@@ -9,6 +9,6 @@ spec:
- name: http - name: http
port: 80 port: 80
protocol: TCP protocol: TCP
targetPort: {{ .apps.ghost.port }} targetPort: {{ .port }}
selector: selector:
component: web component: web

View File

@@ -12,7 +12,7 @@ spec:
spec: spec:
containers: containers:
- name: db-init - name: db-init
image: {{ .apps.postgres.image }} image: postgres:17
command: ["/bin/bash", "-c"] command: ["/bin/bash", "-c"]
args: args:
- | - |
@@ -36,16 +36,16 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: postgres-secrets name: postgres-secrets
key: apps.postgres.password key: password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.gitea.dbHost }}" value: "{{ .dbHost }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
value: "{{ .apps.gitea.dbName }}" value: "{{ .dbName }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .apps.gitea.dbUser }}" value: "{{ .dbUser }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: apps.gitea.dbPassword key: dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

@@ -23,7 +23,7 @@ spec:
terminationGracePeriodSeconds: 60 terminationGracePeriodSeconds: 60
containers: containers:
- name: gitea - name: gitea
image: "{{ .apps.gitea.image }}" image: "{{ .image }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
envFrom: envFrom:
- configMapRef: - configMapRef:
@@ -33,27 +33,27 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: apps.gitea.adminPassword key: adminPassword
- name: GITEA__security__SECRET_KEY - name: GITEA__security__SECRET_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: apps.gitea.secretKey key: secretKey
- name: GITEA__security__INTERNAL_TOKEN - name: GITEA__security__INTERNAL_TOKEN
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: apps.gitea.jwtSecret key: jwtSecret
- name: GITEA__database__PASSWD - name: GITEA__database__PASSWD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: apps.gitea.dbPassword key: dbPassword
- name: GITEA__mailer__PASSWD - name: GITEA__mailer__PASSWD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: apps.gitea.smtpPassword key: smtpPassword
ports: ports:
- name: ssh - name: ssh
containerPort: 2222 containerPort: 2222

View File

@@ -3,12 +3,12 @@ SSH_PORT=22
GITEA_WORK_DIR=/data GITEA_WORK_DIR=/data
GITEA_TEMP=/tmp/gitea GITEA_TEMP=/tmp/gitea
TMPDIR=/tmp/gitea TMPDIR=/tmp/gitea
GITEA_ADMIN_USERNAME={{ .apps.gitea.adminUser }} GITEA_ADMIN_USERNAME={{ .adminUser }}
GITEA_ADMIN_PASSWORD_MODE=keepUpdated GITEA_ADMIN_PASSWORD_MODE=keepUpdated
# Core app settings # Core app settings
GITEA____APP_NAME={{ .apps.gitea.appName }} GITEA____APP_NAME={{ .appName }}
GITEA____RUN_MODE={{ .apps.gitea.runMode }} GITEA____RUN_MODE={{ .runMode }}
GITEA____RUN_USER=git GITEA____RUN_USER=git
# Security settings # Security settings
@@ -17,19 +17,19 @@ GITEA__security__PASSWORD_HASH_ALGO=pbkdf2
# Database settings (except password which comes from secret) # Database settings (except password which comes from secret)
GITEA__database__DB_TYPE=postgres GITEA__database__DB_TYPE=postgres
GITEA__database__HOST={{ .apps.gitea.dbHost }}:{{ .apps.gitea.dbPort }} GITEA__database__HOST={{ .dbHost }}:{{ .dbPort }}
GITEA__database__NAME={{ .apps.gitea.dbName }} GITEA__database__NAME={{ .dbName }}
GITEA__database__USER={{ .apps.gitea.dbUser }} GITEA__database__USER={{ .dbUser }}
GITEA__database__SSL_MODE=disable GITEA__database__SSL_MODE=disable
GITEA__database__LOG_SQL=false GITEA__database__LOG_SQL=false
# Server settings # Server settings
GITEA__server__DOMAIN={{ .apps.gitea.domain }} GITEA__server__DOMAIN={{ .domain }}
GITEA__server__HTTP_PORT={{ .apps.gitea.port }} GITEA__server__HTTP_PORT={{ .port }}
GITEA__server__ROOT_URL=https://{{ .apps.gitea.domain }}/ GITEA__server__ROOT_URL=https://{{ .domain }}/
GITEA__server__DISABLE_SSH=false GITEA__server__DISABLE_SSH=false
GITEA__server__SSH_DOMAIN={{ .apps.gitea.domain }} GITEA__server__SSH_DOMAIN={{ .domain }}
GITEA__server__SSH_PORT={{ .apps.gitea.sshPort }} GITEA__server__SSH_PORT={{ .sshPort }}
GITEA__server__SSH_LISTEN_PORT=2222 GITEA__server__SSH_LISTEN_PORT=2222
GITEA__server__LFS_START_SERVER=true GITEA__server__LFS_START_SERVER=true
GITEA__server__OFFLINE_MODE=true GITEA__server__OFFLINE_MODE=true
@@ -53,8 +53,8 @@ GITEA__webhook__ALLOWED_HOST_LIST=*
# Mailer settings (enabled via env vars, password from secret) # Mailer settings (enabled via env vars, password from secret)
GITEA__mailer__ENABLED=true GITEA__mailer__ENABLED=true
GITEA__mailer__SMTP_ADDR={{ .apps.gitea.smtp.host }} GITEA__mailer__SMTP_ADDR={{ .smtp.host }}
GITEA__mailer__SMTP_PORT={{ .apps.gitea.smtp.port }} GITEA__mailer__SMTP_PORT={{ .smtp.port }}
GITEA__mailer__FROM={{ .apps.gitea.smtp.from }} GITEA__mailer__FROM={{ .smtp.from }}
GITEA__mailer__USER={{ .apps.gitea.smtp.user }} GITEA__mailer__USER={{ .smtp.user }}

View File

@@ -5,10 +5,10 @@ metadata:
namespace: gitea namespace: gitea
annotations: annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: "{{ .cloud.domain }}" external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
spec: spec:
rules: rules:
- host: "{{ .apps.gitea.domain }}" - host: "{{ .domain }}"
http: http:
paths: paths:
- path: / - path: /
@@ -19,6 +19,6 @@ spec:
port: port:
number: 3000 number: 3000
tls: tls:
- secretName: "{{ .apps.gitea.tlsSecretName }}" - secretName: "{{ .tlsSecretName }}"
hosts: hosts:
- "{{ .apps.gitea.domain }}" - "{{ .domain }}"

View File

@@ -1,10 +1,13 @@
name: gitea name: gitea
is: gitea
description: Gitea is a painless self-hosted Git service written in Go description: Gitea is a painless self-hosted Git service written in Go
version: 1.24.3 version: 1.24.3
icon: https://github.com/go-gitea/gitea/raw/main/assets/logo.png icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
requires: requires:
- name: postgres - name: postgres
defaultConfig: defaultConfig:
namespace: gitea
externalDnsDomain: '{{ .cloud.domain }}'
image: gitea/gitea:1.24.3 image: gitea/gitea:1.24.3
appName: Gitea appName: Gitea
domain: gitea.{{ .cloud.domain }} domain: gitea.{{ .cloud.domain }}
@@ -16,18 +19,20 @@ defaultConfig:
dbUser: gitea dbUser: gitea
dbHost: postgres.postgres.svc.cluster.local dbHost: postgres.postgres.svc.cluster.local
adminUser: admin adminUser: admin
adminEmail: "admin@{{ .cloud.domain }}" adminEmail: "{{ .operator.email }}"
dbPort: 5432 dbPort: 5432
timezone: UTC timezone: UTC
runMode: prod runMode: prod
smtp: smtp:
host: "{{ .cloud.smtp.host }}" host: '{{ .cloud.smtp.host }}'
port: "{{ .cloud.smtp.port }}" port: '{{ .cloud.smtp.port }}'
user: "{{ .cloud.smtp.user }}" user: '{{ .cloud.smtp.user }}'
from: "{{ .cloud.smtp.from }}" from: '{{ .cloud.smtp.from }}'
defaultSecrets: defaultSecrets:
- key: apps.gitea.adminPassword - key: adminPassword
- key: apps.gitea.dbPassword - key: dbPassword
- key: apps.gitea.secretKey - key: secretKey
- key: apps.gitea.jwtSecret - key: jwtSecret
- key: apps.gitea.smtpPassword - key: smtpPassword
requiredSecrets:
- postgres.password

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: gitea name: "{{ .namespace }}"

View File

@@ -9,4 +9,4 @@ spec:
storageClassName: longhorn storageClassName: longhorn
resources: resources:
requests: requests:
storage: "{{ .apps.gitea.storage }}" storage: "{{ .storage }}"

View File

@@ -8,7 +8,7 @@ spec:
ports: ports:
- name: http - name: http
port: 3000 port: 3000
targetPort: {{ .apps.gitea.port }} targetPort: {{ .port }}
selector: selector:
component: web component: web
--- ---
@@ -21,7 +21,7 @@ spec:
type: LoadBalancer type: LoadBalancer
ports: ports:
- name: ssh - name: ssh
port: {{ .apps.gitea.sshPort }} port: {{ .sshPort }}
targetPort: 2222 targetPort: 2222
protocol: TCP protocol: TCP
selector: selector:

View File

@@ -7,7 +7,7 @@ spec:
spec: spec:
containers: containers:
- name: db-init - name: db-init
image: {{ .apps.postgres.image }} image: postgres:17
command: ["/bin/bash", "-c"] command: ["/bin/bash", "-c"]
args: args:
- | - |
@@ -53,16 +53,16 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: apps.postgres.password key: postgres.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}" value: "{{ .dbHostname }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
value: "{{ .apps.immich.dbUsername }}" value: "{{ .dbUsername }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .apps.immich.dbUsername }}" value: "{{ .dbUsername }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: apps.immich.dbPassword key: dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

@@ -15,14 +15,14 @@ spec:
component: machine-learning component: machine-learning
spec: spec:
containers: containers:
- image: "{{ .apps.immich.mlImage }}" - image: "{{ .mlImage }}"
name: immich-machine-learning name: immich-machine-learning
ports: ports:
- containerPort: {{ .apps.immich.mlPort }} - containerPort: {{ .mlPort }}
protocol: TCP protocol: TCP
env: env:
- name: TZ - name: TZ
value: "{{ .apps.immich.timezone }}" value: "{{ .timezone }}"
volumeMounts: volumeMounts:
- mountPath: /cache - mountPath: /cache
name: immich-cache name: immich-cache

View File

@@ -20,27 +20,27 @@ spec:
component: microservices component: microservices
spec: spec:
containers: containers:
- image: "{{ .apps.immich.serverImage }}" - image: "{{ .serverImage }}"
name: immich-microservices name: immich-microservices
env: env:
- name: REDIS_HOSTNAME - name: REDIS_HOSTNAME
value: "{{ .apps.immich.redisHostname }}" value: "{{ .redisHostname }}"
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: apps.redis.password key: redis.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}" value: "{{ .dbHostname }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .apps.immich.dbUsername }}" value: "{{ .dbUsername }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: apps.immich.dbPassword key: dbPassword
- name: TZ - name: TZ
value: "{{ .apps.immich.timezone }}" value: "{{ .timezone }}"
- name: IMMICH_WORKERS_EXCLUDE - name: IMMICH_WORKERS_EXCLUDE
value: api value: api
volumeMounts: volumeMounts:

View File

@@ -20,30 +20,30 @@ spec:
component: server component: server
spec: spec:
containers: containers:
- image: "{{ .apps.immich.serverImage }}" - image: "{{ .serverImage }}"
name: immich-server name: immich-server
ports: ports:
- containerPort: {{ .apps.immich.serverPort }} - containerPort: {{ .serverPort }}
protocol: TCP protocol: TCP
env: env:
- name: REDIS_HOSTNAME - name: REDIS_HOSTNAME
value: "{{ .apps.immich.redisHostname }}" value: "{{ .redisHostname }}"
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: apps.redis.password key: redis.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}" value: "{{ .dbHostname }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .apps.immich.dbUsername }}" value: "{{ .dbUsername }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: apps.immich.dbPassword key: dbPassword
- name: TZ - name: TZ
value: "{{ .apps.immich.timezone }}" value: "{{ .timezone }}"
- name: IMMICH_WORKERS_EXCLUDE - name: IMMICH_WORKERS_EXCLUDE
value: microservices value: microservices
volumeMounts: volumeMounts:

View File

@@ -4,11 +4,11 @@ kind: Ingress
metadata: metadata:
name: immich-public name: immich-public
annotations: annotations:
external-dns.alpha.kubernetes.io/target: "{{ .cloud.domain }}" external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec: spec:
rules: rules:
- host: "{{ .apps.immich.domain }}" - host: "{{ .domain }}"
http: http:
paths: paths:
- path: / - path: /
@@ -21,4 +21,4 @@ spec:
tls: tls:
- secretName: wildcard-wild-cloud-tls - secretName: wildcard-wild-cloud-tls
hosts: hosts:
- "{{ .apps.immich.domain }}" - "{{ .domain }}"

View File

@@ -1,12 +1,16 @@
name: immich name: immich
is: immich
install: true install: true
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely. description: Immich is a self-hosted photo and video backup solution that allows you
version: 1.0.0 to store, manage, and share your media files securely.
icon: https://immich.app/assets/images/logo.png version: release
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
requires: requires:
- name: redis - name: redis
- name: postgres - name: postgres
defaultConfig: defaultConfig:
namespace: immich
externalDnsDomain: '{{ .cloud.domain }}'
serverImage: ghcr.io/immich-app/immich-server:release serverImage: ghcr.io/immich-app/immich-server:release
mlImage: ghcr.io/immich-app/immich-machine-learning:release mlImage: ghcr.io/immich-app/immich-machine-learning:release
timezone: UTC timezone: UTC
@@ -20,6 +24,7 @@ defaultConfig:
domain: immich.{{ .cloud.domain }} domain: immich.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
defaultSecrets: defaultSecrets:
- key: apps.immich.dbPassword - key: dbPassword
- key: apps.postgres.password requiredSecrets:
- key: apps.redis.password - redis.password
- postgres.password

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: immich name: "{{ .namespace }}"

View File

@@ -9,7 +9,7 @@ spec:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: {{ .apps.immich.storage }} storage: {{ .storage }}
--- ---
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
@@ -21,4 +21,4 @@ spec:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: {{ .apps.immich.cacheStorage }} storage: {{ .cacheStorage }}

View File

@@ -9,7 +9,7 @@ metadata:
spec: spec:
ports: ports:
- port: 3001 - port: 3001
targetPort: {{ .apps.immich.serverPort }} targetPort: {{ .serverPort }}
selector: selector:
app: immich app: immich
component: server component: server
@@ -25,7 +25,7 @@ metadata:
app: immich-machine-learning app: immich-machine-learning
spec: spec:
ports: ports:
- port: {{ .apps.immich.mlPort }} - port: {{ .mlPort }}
selector: selector:
app: immich app: immich
component: machine-learning component: machine-learning

View File

@@ -26,23 +26,23 @@ spec:
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
env: env:
- name: PGHOST - name: PGHOST
value: {{ .apps.keila.dbHostname }} value: {{ .dbHostname }}
- name: PGUSER - name: PGUSER
value: postgres value: postgres
- name: PGPASSWORD - name: PGPASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: keila-secrets name: keila-secrets
key: apps.postgres.password key: postgres.password
- name: DB_NAME - name: DB_NAME
value: {{ .apps.keila.dbName }} value: {{ .dbName }}
- name: DB_USER - name: DB_USER
value: {{ .apps.keila.dbUsername }} value: {{ .dbUsername }}
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: keila-secrets name: keila-secrets
key: apps.keila.dbPassword key: dbPassword
command: command:
- /bin/bash - /bin/bash
- -c - -c

View File

@@ -14,54 +14,54 @@ spec:
spec: spec:
containers: containers:
- name: keila - name: keila
image: {{ .apps.keila.image }} image: "{{ .image }}"
ports: ports:
- containerPort: {{ .apps.keila.port }} - containerPort: {{ .port }}
env: env:
- name: DB_URL - name: DB_URL
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: keila-secrets name: keila-secrets
key: apps.keila.dbUrl key: dbUrl
- name: URL_HOST - name: URL_HOST
value: {{ .apps.keila.domain }} value: "{{ .domain }}"
- name: URL_SCHEMA - name: URL_SCHEMA
value: https value: https
- name: URL_PORT - name: URL_PORT
value: "443" value: "443"
- name: PORT - name: PORT
value: "{{ .apps.keila.port }}" value: "{{ .port }}"
- name: SECRET_KEY_BASE - name: SECRET_KEY_BASE
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: keila-secrets name: keila-secrets
key: apps.keila.secretKeyBase key: secretKeyBase
- name: MAILER_SMTP_HOST - name: MAILER_SMTP_HOST
value: {{ .apps.keila.smtp.host }} value: "{{ .smtp.host }}"
- name: MAILER_SMTP_PORT - name: MAILER_SMTP_PORT
value: "{{ .apps.keila.smtp.port }}" value: "{{ .smtp.port }}"
- name: MAILER_ENABLE_SSL - name: MAILER_ENABLE_SSL
value: "{{ .apps.keila.smtp.tls }}" value: "{{ .smtp.tls }}"
- name: MAILER_ENABLE_STARTTLS - name: MAILER_ENABLE_STARTTLS
value: "{{ .apps.keila.smtp.startTls }}" value: "{{ .smtp.startTls }}"
- name: MAILER_SMTP_USER - name: MAILER_SMTP_USER
value: {{ .apps.keila.smtp.user }} value: "{{ .smtp.user }}"
- name: MAILER_SMTP_PASSWORD - name: MAILER_SMTP_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: keila-secrets name: keila-secrets
key: apps.keila.smtpPassword key: smtpPassword
- name: MAILER_SMTP_FROM_EMAIL - name: MAILER_SMTP_FROM_EMAIL
value: {{ .apps.keila.smtp.from }} value: "{{ .smtp.from }}"
- name: DISABLE_REGISTRATION - name: DISABLE_REGISTRATION
value: "{{ .apps.keila.disableRegistration }}" value: "{{ .disableRegistration }}"
- name: KEILA_USER - name: KEILA_USER
value: "{{ .apps.keila.adminUser }}" value: "{{ .adminUser }}"
- name: KEILA_PASSWORD - name: KEILA_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: keila-secrets name: keila-secrets
key: apps.keila.adminPassword key: adminPassword
- name: USER_CONTENT_DIR - name: USER_CONTENT_DIR
value: /var/lib/keila/uploads value: /var/lib/keila/uploads
volumeMounts: volumeMounts:
@@ -70,13 +70,13 @@ spec:
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: /
port: {{ .apps.keila.port }} port: {{ .port }}
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 10 periodSeconds: 10
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: /
port: {{ .apps.keila.port }} port: {{ .port }}
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 5 periodSeconds: 5
volumes: volumes:

View File

@@ -5,12 +5,12 @@ metadata:
annotations: annotations:
traefik.ingress.kubernetes.io/router.tls: "true" traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }} external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
traefik.ingress.kubernetes.io/router.middlewares: keila-cors@kubernetescrd traefik.ingress.kubernetes.io/router.middlewares: keila-cors@kubernetescrd
spec: spec:
rules: rules:
- host: {{ .apps.keila.domain }} - host: {{ .domain }}
http: http:
paths: paths:
- path: / - path: /
@@ -23,4 +23,4 @@ spec:
tls: tls:
- secretName: "wildcard-wild-cloud-tls" - secretName: "wildcard-wild-cloud-tls"
hosts: hosts:
- "{{ .apps.keila.domain }}" - "{{ .domain }}"

View File

@@ -1,15 +1,19 @@
name: keila name: keila
is: keila
description: Keila is an open-source email marketing platform that allows you to send newsletters and manage mailing lists with privacy and control. description: Keila is an open-source email marketing platform that allows you to send newsletters and manage mailing lists with privacy and control.
version: 1.0.0 version: 0.17.1
icon: https://www.keila.io/images/logo.svg icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg
requires: requires:
- name: postgres - name: postgres
defaultConfig: defaultConfig:
image: pentacent/keila:latest namespace: keila
externalDnsDomain: "{{ .cloud.domain }}"
image: pentacent/keila:0.17.1
port: 4000 port: 4000
storage: 1Gi storage: 1Gi
domain: keila.{{ .cloud.domain }} domain: keila.{{ .cloud.domain }}
dbHostname: postgres.postgres.svc.cluster.local dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbName: keila dbName: keila
dbUsername: keila dbUsername: keila
disableRegistration: "true" disableRegistration: "true"
@@ -20,12 +24,15 @@ defaultConfig:
port: "{{ .cloud.smtp.port }}" port: "{{ .cloud.smtp.port }}"
from: "{{ .cloud.smtp.from }}" from: "{{ .cloud.smtp.from }}"
user: "{{ .cloud.smtp.user }}" user: "{{ .cloud.smtp.user }}"
tls: {{ .cloud.smtp.tls }} tls: "{{ .cloud.smtp.tls }}"
startTls: {{ .cloud.smtp.startTls }} startTls: "{{ .cloud.smtp.startTls }}"
defaultSecrets: defaultSecrets:
- key: apps.keila.secretKeyBase - key: secretKeyBase
- key: apps.keila.dbPassword default: "{{ random.AlphaNum 64 }}"
- key: apps.keila.dbUrl - key: dbPassword
- key: apps.keila.adminPassword - key: dbUrl
- key: apps.keila.smtpPassword default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/keila?sslmode=disable"
- key: apps.postgres.password - key: adminPassword
- key: smtpPassword
requiredSecrets:
- postgres.password

View File

@@ -21,8 +21,8 @@ spec:
- "OPTIONS" - "OPTIONS"
accessControlAllowOriginList: accessControlAllowOriginList:
- "http://localhost:1313" - "http://localhost:1313"
- "https://*.{{ .cloud.domain }}" - "https://*.{{ .externalDnsDomain }}"
- "https://{{ .cloud.domain }}" - "https://{{ .externalDnsDomain }}"
accessControlExposeHeaders: accessControlExposeHeaders:
- "*" - "*"
accessControlMaxAge: 86400 accessControlMaxAge: 86400

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: keila name: "{{ .namespace }}"

View File

@@ -7,4 +7,4 @@ spec:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: {{ .apps.keila.storage }} storage: {{ .storage }}

View File

@@ -7,5 +7,5 @@ spec:
component: web component: web
ports: ports:
- port: 80 - port: 80
targetPort: {{ .apps.keila.port }} targetPort: {{ .port }}
protocol: TCP protocol: TCP

36
lemmy/configmap.yaml Normal file
View File

@@ -0,0 +1,36 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: lemmy-config
namespace: {{ .namespace }}
data:
lemmy.hjson: |
{
hostname: "{{ .domain }}"
bind: "0.0.0.0"
port: {{ .backendPort }}
tls_enabled: false
database: {
uri: "postgresql://{{ .dbUser }}:DBPASSWORD@{{ .dbHost }}:{{ .dbPort }}/{{ .dbName }}"
}
pictrs: {
url: "http://lemmy-pictrs:{{ .pictrsPort }}/"
api_key: "PICTRS_API_KEY"
}
email: {
smtp_server: "{{ .smtp.host }}:{{ .smtp.port }}"
smtp_login: "{{ .smtp.user }}"
smtp_password: "SMTP_PASSWORD"
smtp_from_address: "{{ .smtp.from }}"
tls_type: "{{ if eq .smtp.tls "true" }}tls{{ else }}none{{ end }}"
}
setup: {
admin_username: "admin"
admin_password: "ADMIN_PASSWORD"
site_name: "Lemmy"
}
}

76
lemmy/db-init-job.yaml Normal file
View File

@@ -0,0 +1,76 @@
apiVersion: batch/v1
kind: Job
metadata:
name: lemmy-db-init
namespace: {{ .namespace }}
spec:
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: db-init
image: postgres:16-alpine
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: "{{ .dbHost }}"
- name: PGPORT
value: "{{ .dbPort }}"
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: lemmy-secrets
key: postgres.password
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUser }}"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: lemmy-secrets
key: dbPassword
command:
- sh
- -c
- |
set -e
echo "Waiting for PostgreSQL to be ready..."
until pg_isready -h $PGHOST -p $PGPORT -U $PGUSER; do
echo "Waiting for database connection..."
sleep 2
done
echo "Creating database and user..."
psql -v ON_ERROR_STOP=1 <<-EOSQL
SELECT 'CREATE DATABASE ${DB_NAME}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME}')\gexec
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '${DB_USER}') THEN
CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';
ELSE
ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';
END IF;
END
\$\$;
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
\c ${DB_NAME}
GRANT ALL ON SCHEMA public TO ${DB_USER};
EOSQL
echo "Database initialization completed successfully"

View File

@@ -0,0 +1,102 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: lemmy-backend
namespace: {{ .namespace }}
spec:
replicas: {{ .backendReplicas }}
selector:
matchLabels:
component: backend
template:
metadata:
labels:
component: backend
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: config-prep
image: busybox:stable
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
command:
- sh
- -c
- |
cp /config-template/lemmy.hjson /config/lemmy.hjson
sed -i "s|DBPASSWORD|${DB_PASSWORD}|g" /config/lemmy.hjson
sed -i "s|PICTRS_API_KEY|${PICTRS_API_KEY}|g" /config/lemmy.hjson
sed -i "s|SMTP_PASSWORD|${SMTP_PASSWORD}|g" /config/lemmy.hjson
sed -i "s|ADMIN_PASSWORD|${ADMIN_PASSWORD}|g" /config/lemmy.hjson
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: lemmy-secrets
key: dbPassword
- name: PICTRS_API_KEY
valueFrom:
secretKeyRef:
name: lemmy-secrets
key: jwtSecret
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: lemmy-secrets
key: smtpPassword
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: lemmy-secrets
key: adminPassword
volumeMounts:
- name: config-template
mountPath: /config-template
- name: config
mountPath: /config
containers:
- name: backend
image: {{ .backendImage }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
env:
- name: LEMMY_CONFIG_LOCATION
value: /config/lemmy.hjson
- name: TZ
value: "{{ .timezone }}"
ports:
- containerPort: {{ .backendPort }}
name: http
volumeMounts:
- name: config
mountPath: /config
livenessProbe:
httpGet:
path: /api/v3/site
port: {{ .backendPort }}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v3/site
port: {{ .backendPort }}
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: config-template
configMap:
name: lemmy-config
- name: config
emptyDir: {}

View File

@@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: lemmy-pictrs
namespace: {{ .namespace }}
spec:
replicas: {{ .pictrsReplicas }}
selector:
matchLabels:
component: pictrs
template:
metadata:
labels:
component: pictrs
spec:
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: pictrs
image: {{ .pictrsImage }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
env:
- name: PICTRS__SERVER__BIND
value: "0.0.0.0:{{ .pictrsPort }}"
- name: PICTRS__MEDIA__VIDEO_CODEC
value: vp9
- name: PICTRS__MEDIA__GIF__MAX_WIDTH
value: "256"
- name: PICTRS__MEDIA__GIF__MAX_HEIGHT
value: "256"
- name: PICTRS__MEDIA__GIF__MAX_AREA
value: "65536"
- name: PICTRS__MEDIA__GIF__MAX_FRAME_COUNT
value: "400"
- name: RUST_LOG
value: debug
- name: RUST_BACKTRACE
value: full
- name: PICTRS__REPO__TYPE
value: sled
- name: PICTRS__REPO__PATH
value: /mnt/sled-repo
- name: PICTRS__STORE__TYPE
value: filesystem
- name: PICTRS__STORE__PATH
value: /mnt/files
ports:
- containerPort: {{ .pictrsPort }}
name: http
volumeMounts:
- name: storage
mountPath: /mnt
livenessProbe:
httpGet:
path: /healthz
port: {{ .pictrsPort }}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: {{ .pictrsPort }}
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: storage
persistentVolumeClaim:
claimName: lemmy-pictrs-storage

51
lemmy/deployment-ui.yaml Normal file
View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: lemmy-ui
namespace: {{ .namespace }}
spec:
replicas: {{ .uiReplicas }}
selector:
matchLabels:
component: ui
template:
metadata:
labels:
component: ui
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: ui
image: {{ .uiImage }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
env:
- name: LEMMY_UI_LEMMY_INTERNAL_HOST
value: "lemmy-backend:{{ .backendPort }}"
- name: LEMMY_UI_LEMMY_EXTERNAL_HOST
value: "{{ .domain }}"
- name: LEMMY_UI_HTTPS
value: "true"
ports:
- containerPort: {{ .uiPort }}
name: http
livenessProbe:
httpGet:
path: /
port: {{ .uiPort }}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: {{ .uiPort }}
initialDelaySeconds: 10
periodSeconds: 5

42
lemmy/ingress.yaml Normal file
View File

@@ -0,0 +1,42 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lemmy-ingress
namespace: {{ .namespace }}
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}
rules:
- host: {{ .domain }}
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: lemmy-backend
port:
number: {{ .backendPort }}
- path: /pictrs
pathType: Prefix
backend:
service:
name: lemmy-pictrs
port:
number: {{ .pictrsPort }}
- path: /
pathType: Prefix
backend:
service:
name: lemmy-ui
port:
number: {{ .uiPort }}

21
lemmy/kustomization.yaml Normal file
View File

@@ -0,0 +1,21 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: lemmy
labels:
- includeSelectors: true
pairs:
app: lemmy
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- configmap.yaml
- pvc-pictrs.yaml
- db-init-job.yaml
- deployment-pictrs.yaml
- service-pictrs.yaml
- deployment-backend.yaml
- service-backend.yaml
- deployment-ui.yaml
- service-ui.yaml
- ingress.yaml

41
lemmy/manifest.yaml Normal file
View File

@@ -0,0 +1,41 @@
name: lemmy
is: lemmy
description: Lemmy is a selfhosted social link aggregation and discussion platform. It is an open source alternative to Reddit, designed for the fediverse.
version: 0.19.15
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lemmy.svg
requires:
- name: postgres
defaultConfig:
namespace: lemmy
backendImage: dessalines/lemmy:0.19.15
uiImage: dessalines/lemmy-ui:0.19.15
pictrsImage: asonix/pictrs:0.5.5
backendPort: 8536
uiPort: 1234
pictrsPort: 8080
backendReplicas: 1
uiReplicas: 1
pictrsReplicas: 1
storage: 10Gi
pictrsStorage: 50Gi
timezone: UTC
domain: lemmy.{{ .cloud.domain }}
externalDnsDomain: lemmy.{{ .cloud.baseDomain }}
tlsSecretName: lemmy-tls
dbName: lemmy
dbUser: lemmy
dbHost: postgres.postgres.svc.cluster.local
dbPort: 5432
smtp:
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
from: "noreply@{{ .cloud.baseDomain }}"
tls: "{{ .cloud.smtp.tls }}"
defaultSecrets:
- key: dbPassword
- key: adminPassword
- key: jwtSecret
- key: smtpPassword
requiredSecrets:
- postgres.password

4
lemmy/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .namespace }}

11
lemmy/pvc-pictrs.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lemmy-pictrs-storage
namespace: {{ .namespace }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .pictrsStorage }}

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: lemmy-backend
namespace: {{ .namespace }}
spec:
type: ClusterIP
selector:
component: backend
ports:
- name: http
port: {{ .backendPort }}
targetPort: {{ .backendPort }}

13
lemmy/service-pictrs.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: lemmy-pictrs
namespace: {{ .namespace }}
spec:
type: ClusterIP
selector:
component: pictrs
ports:
- name: http
port: {{ .pictrsPort }}
targetPort: {{ .pictrsPort }}

13
lemmy/service-ui.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: lemmy-ui
namespace: {{ .namespace }}
spec:
type: ClusterIP
selector:
component: ui
ports:
- name: http
port: {{ .uiPort }}
targetPort: {{ .uiPort }}

View File

@@ -28,23 +28,23 @@ spec:
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
env: env:
- name: PGHOST - name: PGHOST
value: {{ .apps.listmonk.dbHost }} value: {{ .dbHost }}
- name: PGUSER - name: PGUSER
value: postgres value: postgres
- name: PGPASSWORD - name: PGPASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: listmonk-secrets name: listmonk-secrets
key: apps.postgres.password key: postgres.password
- name: DB_NAME - name: DB_NAME
value: {{ .apps.listmonk.dbName }} value: {{ .dbName }}
- name: DB_USER - name: DB_USER
value: {{ .apps.listmonk.dbUser }} value: {{ .dbUser }}
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: listmonk-secrets name: listmonk-secrets
key: apps.listmonk.dbPassword key: dbPassword
command: command:
- /bin/bash - /bin/bash
- -c - -c

View File

@@ -30,21 +30,23 @@ spec:
env: env:
- name: LISTMONK_app__address - name: LISTMONK_app__address
value: "0.0.0.0:9000" value: "0.0.0.0:9000"
- name: LISTMONK_app__root_url
value: "{{ .rootUrl }}"
- name: LISTMONK_db__host - name: LISTMONK_db__host
value: {{ .apps.listmonk.dbHost }} value: {{ .dbHost }}
- name: LISTMONK_db__port - name: LISTMONK_db__port
value: "{{ .apps.listmonk.dbPort }}" value: "{{ .dbPort }}"
- name: LISTMONK_db__user - name: LISTMONK_db__user
value: {{ .apps.listmonk.dbUser }} value: {{ .dbUser }}
- name: LISTMONK_db__database - name: LISTMONK_db__database
value: {{ .apps.listmonk.dbName }} value: {{ .dbName }}
- name: LISTMONK_db__ssl_mode - name: LISTMONK_db__ssl_mode
value: {{ .apps.listmonk.dbSSLMode }} value: {{ .dbSSLMode }}
- name: LISTMONK_db__password - name: LISTMONK_db__password
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: listmonk-secrets name: listmonk-secrets
key: apps.listmonk.dbPassword key: dbPassword
resources: resources:
limits: limits:
cpu: 500m cpu: 500m

View File

@@ -6,16 +6,16 @@ metadata:
annotations: annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true" traefik.ingress.kubernetes.io/router.tls: "true"
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }} external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec: spec:
ingressClassName: traefik ingressClassName: traefik
tls: tls:
- hosts: - hosts:
- {{ .apps.listmonk.domain }} - {{ .domain }}
secretName: {{ .apps.listmonk.tlsSecretName }} secretName: {{ .tlsSecretName }}
rules: rules:
- host: {{ .apps.listmonk.domain }} - host: {{ .domain }}
http: http:
paths: paths:
- path: / - path: /

View File

@@ -1,11 +1,16 @@
name: listmonk name: listmonk
description: Listmonk is a standalone, self-hosted, newsletter and mailing list manager. It is fast, feature-rich, and packed into a single binary. is: listmonk
description: Listmonk is a standalone, self-hosted, newsletter and mailing list manager.
It is fast, feature-rich, and packed into a single binary.
version: 5.0.3 version: 5.0.3
icon: https://listmonk.app/static/images/logo.svg icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/listmonk.svg
requires: requires:
- name: postgres - name: postgres
defaultConfig: defaultConfig:
namespace: listmonk
externalDnsDomain: '{{ .cloud.domain }}'
domain: listmonk.{{ .cloud.domain }} domain: listmonk.{{ .cloud.domain }}
rootUrl: https://listmonk.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi storage: 1Gi
dbHost: postgres.postgres.svc.cluster.local dbHost: postgres.postgres.svc.cluster.local
@@ -15,6 +20,8 @@ defaultConfig:
dbSSLMode: disable dbSSLMode: disable
timezone: UTC timezone: UTC
defaultSecrets: defaultSecrets:
- key: apps.listmonk.dbPassword - key: dbPassword
- key: apps.listmonk.dbUrl - key: dbUrl
- key: apps.postgres.password default: 'postgres://{{ .app.dbUser }}:{{ .secrets.dbPassword }}@{{ .app.dbHost }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode={{ .app.dbSSLMode }}'
requiredSecrets:
- postgres.password

View File

@@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: listmonk name: "{{ .namespace }}"

View File

@@ -8,4 +8,4 @@ spec:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: {{ .apps.listmonk.storage }} storage: {{ .storage }}

View File

@@ -8,57 +8,48 @@ spec:
restartPolicy: OnFailure restartPolicy: OnFailure
containers: containers:
- name: db-init - name: db-init
image: postgres:15-alpine image: {{ .image }}
command:
- /bin/bash
- -c
- |
set -e
echo "Initializing Loomio database..."
# Patch schema.rb to use IF NOT EXISTS for pghero schema
sed -i 's/create_schema "pghero"/execute "CREATE SCHEMA IF NOT EXISTS pghero"/g' db/schema.rb
bundle exec rake db:schema:load db:seed
echo "Database initialization complete"
env: env:
- name: PGHOST - name: RAILS_ENV
value: "{{ .db.host }}" value: production
- name: PGPORT - name: DATABASE_URL
value: "{{ .db.port }}"
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-secrets
key: postgres.password
- name: LOOMIO_DB_NAME
value: "{{ .db.name }}"
- name: LOOMIO_DB_USER
value: "{{ .db.user }}"
- name: LOOMIO_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: loomio-secrets name: loomio-secrets
key: dbPassword key: dbUrl
command: - name: REDIS_URL
- sh value: {{ .redisUrl }}
- -c - name: DEVISE_SECRET
- | valueFrom:
echo "Creating database and user for Loomio..." secretKeyRef:
name: loomio-secrets
# Check if database exists, create if not key: deviseSecret
psql -tc "SELECT 1 FROM pg_database WHERE datname = '$LOOMIO_DB_NAME'" | grep -q 1 || \ - name: SECRET_COOKIE_TOKEN
psql -c "CREATE DATABASE \"$LOOMIO_DB_NAME\"" valueFrom:
secretKeyRef:
# Check if user exists, create or update password name: loomio-secrets
psql -tc "SELECT 1 FROM pg_user WHERE usename = '$LOOMIO_DB_USER'" | grep -q 1 && \ key: secretCookieToken
psql -c "ALTER USER \"$LOOMIO_DB_USER\" WITH PASSWORD '$LOOMIO_DB_PASSWORD'" || \
psql -c "CREATE USER \"$LOOMIO_DB_USER\" WITH PASSWORD '$LOOMIO_DB_PASSWORD'"
# Grant all privileges
psql -c "GRANT ALL PRIVILEGES ON DATABASE \"$LOOMIO_DB_NAME\" TO \"$LOOMIO_DB_USER\""
# Connect to the database and grant schema permissions
psql -d "$LOOMIO_DB_NAME" -c "GRANT ALL ON SCHEMA public TO \"$LOOMIO_DB_USER\""
echo "Database initialization complete!"
securityContext: securityContext:
runAsNonRoot: true runAsNonRoot: false
runAsUser: 999 # postgres user runAsUser: 0
runAsGroup: 999
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop: [ALL] drop: [ALL]
readOnlyRootFilesystem: true readOnlyRootFilesystem: false
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
securityContext:
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault

View File

@@ -66,6 +66,8 @@ spec:
value: "{{ .smtp.tls }}" value: "{{ .smtp.tls }}"
- name: REPLY_HOSTNAME - name: REPLY_HOSTNAME
value: {{ .smtp.from }} value: {{ .smtp.from }}
- name: BUNDLE_APP_CONFIG
value: /loomio/tmp/.bundle
volumeMounts: volumeMounts:
- name: uploads - name: uploads
mountPath: /loomio/public/system mountPath: /loomio/public/system
@@ -73,6 +75,8 @@ spec:
mountPath: /loomio/storage mountPath: /loomio/storage
- name: tmp - name: tmp
mountPath: /loomio/tmp mountPath: /loomio/tmp
- name: log
mountPath: /loomio/log
resources: resources:
requests: requests:
memory: 256Mi memory: 256Mi
@@ -81,9 +85,8 @@ spec:
memory: 1Gi memory: 1Gi
cpu: 500m cpu: 500m
securityContext: securityContext:
runAsNonRoot: true runAsNonRoot: false
runAsUser: 1000 runAsUser: 0
runAsGroup: 1000
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop: [ALL] drop: [ALL]
@@ -99,3 +102,5 @@ spec:
claimName: loomio-storage claimName: loomio-storage
- name: tmp - name: tmp
emptyDir: {} emptyDir: {}
- name: log
emptyDir: {}

View File

@@ -15,6 +15,13 @@ spec:
containers: containers:
- name: loomio - name: loomio
image: {{ .image }} image: {{ .image }}
command:
- /bin/bash
- -c
- |
set -e
bundle exec rake db:schema:load db:seed
bundle exec thrust puma -C config/puma.rb
ports: ports:
- containerPort: 3000 - containerPort: 3000
name: http name: http
@@ -73,10 +80,12 @@ spec:
secretKeyRef: secretKeyRef:
name: loomio-secrets name: loomio-secrets
key: smtpPassword key: smtpPassword
- name: SMTP_USE_SSL
value: "{{ .smtp.tls }}"
- name: REPLY_HOSTNAME - name: REPLY_HOSTNAME
value: {{ .smtp.from }} value: {{ .smtp.from }}
- name: CHANNELS_URI
value: wss://{{ .domain }}
- name: BUNDLE_APP_CONFIG
value: /loomio/tmp/.bundle
volumeMounts: volumeMounts:
- name: uploads - name: uploads
mountPath: /loomio/public/system mountPath: /loomio/public/system
@@ -84,6 +93,8 @@ spec:
mountPath: /loomio/storage mountPath: /loomio/storage
- name: tmp - name: tmp
mountPath: /loomio/tmp mountPath: /loomio/tmp
- name: log
mountPath: /loomio/log
resources: resources:
requests: requests:
memory: 512Mi memory: 512Mi
@@ -92,21 +103,18 @@ spec:
memory: 2Gi memory: 2Gi
cpu: 1000m cpu: 1000m
livenessProbe: livenessProbe:
httpGet: tcpSocket:
path: /health
port: 3000 port: 3000
initialDelaySeconds: 60 initialDelaySeconds: 60
periodSeconds: 30 periodSeconds: 30
readinessProbe: readinessProbe:
httpGet: tcpSocket:
path: /health
port: 3000 port: 3000
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 10 periodSeconds: 10
securityContext: securityContext:
runAsNonRoot: true runAsNonRoot: false
runAsUser: 1000 runAsUser: 0
runAsGroup: 1000
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop: [ALL] drop: [ALL]
@@ -122,3 +130,5 @@ spec:
claimName: loomio-storage claimName: loomio-storage
- name: tmp - name: tmp
emptyDir: {} emptyDir: {}
- name: log
emptyDir: {}

View File

@@ -1,27 +1,28 @@
name: loomio name: loomio
is: loomio
description: Loomio is a collaborative decision-making tool that makes it easy for groups to make decisions together description: Loomio is a collaborative decision-making tool that makes it easy for groups to make decisions together
version: 3.0.11 version: 3.0.11
icon: https://www.loomio.com/favicon.ico icon: https://www.loomio.com/brand/logo_gold.svg
requires: requires:
- name: postgres - name: postgres
installed_as: postgres installed_as: postgres
- name: redis - name: redis
defaultConfig: defaultConfig:
namespace: loomio namespace: loomio
externalDnsDomain: {{ .cloud.domain }} externalDnsDomain: "{{ .cloud.domain }}"
image: loomio/loomio:v3.0.11 image: loomio/loomio:latest
workerImage: loomio/loomio:v3.0.11 workerImage: loomio/loomio:latest
appName: Loomio appName: Loomio
domain: loomio.{{ .cloud.domain }} domain: "loomio.{{ .cloud.domain }}"
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
port: 3000 port: 3000
storage: storage:
uploads: 5Gi uploads: 5Gi
files: 5Gi files: 5Gi
plugins: 1Gi plugins: 1Gi
redisUrl: {{ .apps.redis.uri }} redisUrl: "{{ .apps.redis.uri }}"
adminEmail: "admin@{{ .cloud.domain }}" adminEmail: "{{ .operator.email }}"
supportEmail: "support@{{ .cloud.domain }}" supportEmail: "{{ .operator.email }}"
forceSSL: "1" forceSSL: "1"
useRackAttack: "1" useRackAttack: "1"
pumaWorkers: "2" pumaWorkers: "2"
@@ -31,8 +32,8 @@ defaultConfig:
db: db:
name: loomio name: loomio
user: loomio user: loomio
host: {{ .apps.postgres.host }} host: "{{ .apps.postgres.host }}"
port: {{ .apps.postgres.port }} port: "{{ .apps.postgres.port }}"
smtp: smtp:
auth: plain auth: plain
domain: "{{ .cloud.domain }}" domain: "{{ .cloud.domain }}"
@@ -51,5 +52,6 @@ defaultSecrets:
- key: secretCookieToken - key: secretCookieToken
default: "{{ random.AlphaNum 32 }}" default: "{{ random.AlphaNum 32 }}"
- key: smtpPassword - key: smtpPassword
default: "{{ .secrets.smtp.password }}"
requiredSecrets: requiredSecrets:
- postgres.password - postgres.password

View File

@@ -4,7 +4,7 @@ metadata:
name: loomio-storage name: loomio-storage
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteMany
resources: resources:
requests: requests:
storage: {{ .storage.files }} storage: {{ .storage.files }}

View File

@@ -4,7 +4,7 @@ metadata:
name: loomio-uploads name: loomio-uploads
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteMany
resources: resources:
requests: requests:
storage: {{ .storage.uploads }} storage: {{ .storage.uploads }}

81
mastodon/README.md Normal file
View File

@@ -0,0 +1,81 @@
# Mastodon
Mastodon is a free, open-source social network server based on ActivityPub. It allows you to run your own instance of a decentralized social media platform.
## Version
This package deploys Mastodon v4.5.3 (released July 8, 2025).
## Dependencies
- **PostgreSQL**: Database for storing application data
- **Redis**: Used for caching and background job queuing
## Configuration
### VAPID Keys
Mastodon requires VAPID (Voluntary Application Server Identification) keys for Web Push notifications. These keys use Elliptic Curve P-256 cryptography.
**The Wild Cloud API automatically generates proper VAPID keys when you add the Mastodon app.** No manual configuration is required!
### Database
The database is automatically initialized with:
- Database: `mastodon_production`
- User: `mastodon` with auto-generated password
- All necessary privileges granted
The db-init job handles creating the database and user, and automatically updates the user password if it changes.
### Storage
Mastodon uses two persistent volumes:
- **Assets** (10Gi): Stores compiled assets and static files
- **System** (100Gi): Stores user uploads, media files, and other system data
Both volumes use ReadWriteMany access mode to allow multiple pods to access them simultaneously.
## Components
Mastodon runs three separate services:
- **Web (Puma)**: Main web server for the Mastodon web interface
- **Streaming (Node.js)**: Real-time streaming API for live updates
- **Sidekiq**: Background job processor for async tasks
## Access
After deployment, Mastodon will be available at:
- https://mastodon.{your-cloud-domain}
The ingress automatically routes:
- `/api/v1/streaming` → Streaming service
- All other paths → Web service
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add mastodon
wild app deploy mastodon
```
2. Generate and configure VAPID keys (see above)
3. Access your instance in a browser and create the first admin user account
4. Configure additional settings through the Mastodon admin interface
## Security
All containers run as non-root user (UID 991) with:
- No privilege escalation
- All capabilities dropped
- Compliant with Pod Security Standards
## Notes
- SMTP configuration is inherited from your Wild Cloud instance settings
- Database credentials are auto-generated and stored in your instance's `secrets.yaml`
- The Active Record Encryption keys are auto-generated for Rails 8.0.3 compatibility

185
mastodon/db-init-job.yaml Normal file
View File

@@ -0,0 +1,185 @@
apiVersion: batch/v1
kind: Job
metadata:
name: mastodon-db-init
namespace: {{ .namespace }}
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: db-init
image: postgres:16-alpine
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: "{{ .dbHostname }}"
- name: PGPORT
value: "{{ .dbPort }}"
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: postgres.password
- name: MASTODON_DB
value: "{{ .dbName }}"
- name: MASTODON_USER
value: "{{ .dbUsername }}"
- name: MASTODON_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
command:
- sh
- -c
- |
set -e
echo "Waiting for PostgreSQL to be ready..."
until pg_isready -h $PGHOST -p $PGPORT -U $PGUSER; do
echo "PostgreSQL is unavailable - sleeping"
sleep 2
done
echo "PostgreSQL is ready"
echo "Creating database if it doesn't exist..."
psql -v ON_ERROR_STOP=1 <<-EOSQL
SELECT 'CREATE DATABASE $MASTODON_DB'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$MASTODON_DB')\gexec
EOSQL
echo "Creating/updating user..."
psql -v ON_ERROR_STOP=1 <<-EOSQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_user WHERE usename = '$MASTODON_USER') THEN
CREATE USER $MASTODON_USER WITH PASSWORD '$MASTODON_PASSWORD';
ELSE
ALTER USER $MASTODON_USER WITH PASSWORD '$MASTODON_PASSWORD';
END IF;
END
\$\$;
EOSQL
echo "Granting privileges..."
psql -v ON_ERROR_STOP=1 <<-EOSQL
GRANT ALL PRIVILEGES ON DATABASE $MASTODON_DB TO $MASTODON_USER;
\c $MASTODON_DB
GRANT ALL ON SCHEMA public TO $MASTODON_USER;
EOSQL
echo "Database initialization complete"
---
apiVersion: batch/v1
kind: Job
metadata:
name: mastodon-db-migrate
namespace: {{ .namespace }}
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
component: db-migrate
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: db-migrate
image: {{ .image }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
command:
- bundle
- exec
- rails
- db:migrate
env:
- name: LOCAL_DOMAIN
value: "{{ .domain }}"
- name: RAILS_ENV
value: production
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: secretKeyBase
- name: OTP_SECRET
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: otpSecret
- name: ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordPrimaryKey
- name: ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordDeterministicKey
- name: ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordKeyDerivationSalt
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
volumeMounts:
- name: assets
mountPath: /opt/mastodon/public/assets
- name: system
mountPath: /opt/mastodon/public/system
volumes:
- name: assets
persistentVolumeClaim:
claimName: mastodon-assets
- name: system
persistentVolumeClaim:
claimName: mastodon-system

View File

@@ -0,0 +1,156 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mastodon-sidekiq
namespace: {{ .namespace }}
spec:
replicas: {{ .sidekiq.replicas }}
selector:
matchLabels:
component: sidekiq
template:
metadata:
labels:
component: sidekiq
spec:
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: sidekiq
image: {{ .image }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
command:
- bundle
- exec
- sidekiq
- -c
- "{{ .sidekiq.concurrency }}"
- -q
- default,8
- -q
- push,6
- -q
- ingress,4
- -q
- mailers,2
- -q
- pull
- -q
- scheduler
env:
- name: LOCAL_DOMAIN
value: "{{ .domain }}"
- name: RAILS_ENV
value: production
- name: RAILS_LOG_LEVEL
value: info
- name: DEFAULT_LOCALE
value: "{{ .locale }}"
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: secretKeyBase
- name: OTP_SECRET
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: otpSecret
- name: VAPID_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPrivateKey
- name: VAPID_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPublicKey
- name: ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordPrimaryKey
- name: ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordDeterministicKey
- name: ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordKeyDerivationSalt
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: postgres.password
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
- name: SMTP_SERVER
value: "{{ .smtp.server }}"
- name: SMTP_PORT
value: "{{ .smtp.port }}"
- name: SMTP_LOGIN
value: "{{ .smtp.user }}"
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: smtpPassword
- name: SMTP_FROM_ADDRESS
value: "{{ .smtp.from }}"
- name: SMTP_AUTH_METHOD
value: "{{ .smtp.authMethod }}"
- name: SMTP_ENABLE_STARTTLS
value: "{{ .smtp.enableStarttls }}"
- name: SMTP_TLS
value: "{{ .smtp.tls }}"
volumeMounts:
- name: assets
mountPath: /opt/mastodon/public/assets
- name: system
mountPath: /opt/mastodon/public/system
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 768Mi
volumes:
- name: assets
persistentVolumeClaim:
claimName: mastodon-assets
- name: system
persistentVolumeClaim:
claimName: mastodon-system

View File

@@ -0,0 +1,83 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mastodon-streaming
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: streaming
template:
metadata:
labels:
component: streaming
spec:
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: streaming
image: {{ .streamingImage }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
ports:
- name: streaming
containerPort: {{ .streamingPort }}
protocol: TCP
env:
- name: NODE_ENV
value: production
- name: PORT
value: "{{ .streamingPort }}"
- name: STREAMING_CLUSTER_NUM
value: "1"
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
memory: 512Mi
livenessProbe:
httpGet:
path: /api/v1/streaming/health
port: streaming
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /api/v1/streaming/health
port: streaming
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 3

View File

@@ -0,0 +1,170 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mastodon-web
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: {{ .image }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
command:
- bundle
- exec
- puma
- -C
- config/puma.rb
ports:
- name: http
containerPort: {{ .webPort }}
protocol: TCP
env:
- name: LOCAL_DOMAIN
value: "{{ .domain }}"
- name: RAILS_ENV
value: production
- name: RAILS_LOG_LEVEL
value: info
- name: DEFAULT_LOCALE
value: "{{ .locale }}"
- name: SINGLE_USER_MODE
value: "{{ .singleUserMode }}"
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: secretKeyBase
- name: OTP_SECRET
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: otpSecret
- name: VAPID_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPrivateKey
- name: VAPID_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPublicKey
- name: ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordPrimaryKey
- name: ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordDeterministicKey
- name: ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: activeRecordKeyDerivationSalt
- name: DB_HOST
value: "{{ .dbHostname }}"
- name: DB_PORT
value: "{{ .dbPort }}"
- name: DB_NAME
value: "{{ .dbName }}"
- name: DB_USER
value: "{{ .dbUsername }}"
- name: DB_PASS
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: dbPassword
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: postgres.password
- name: REDIS_HOST
value: "{{ .redisHostname }}"
- name: REDIS_PORT
value: "{{ .redisPort }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: redis.password
- name: SMTP_SERVER
value: "{{ .smtp.server }}"
- name: SMTP_PORT
value: "{{ .smtp.port }}"
- name: SMTP_LOGIN
value: "{{ .smtp.user }}"
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: smtpPassword
- name: SMTP_FROM_ADDRESS
value: "{{ .smtp.from }}"
- name: SMTP_AUTH_METHOD
value: "{{ .smtp.authMethod }}"
- name: SMTP_ENABLE_STARTTLS
value: "{{ .smtp.enableStarttls }}"
- name: SMTP_TLS
value: "{{ .smtp.tls }}"
- name: STREAMING_API_BASE_URL
value: "wss://{{ .domain }}"
- name: WEB_CONCURRENCY
value: "2"
- name: MAX_THREADS
value: "5"
volumeMounts:
- name: assets
mountPath: /opt/mastodon/public/assets
- name: system
mountPath: /opt/mastodon/public/system
resources:
requests:
cpu: 250m
memory: 768Mi
limits:
memory: 1280Mi
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 3
volumes:
- name: assets
persistentVolumeClaim:
claimName: mastodon-assets
- name: system
persistentVolumeClaim:
claimName: mastodon-system

33
mastodon/ingress.yaml Normal file
View File

@@ -0,0 +1,33 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mastodon
namespace: {{ .namespace }}
annotations:
external-dns.alpha.kubernetes.io/target: {{ .externalDnsDomain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .domain }}
secretName: {{ .tlsSecretName }}
rules:
- host: {{ .domain }}
http:
paths:
- path: /api/v1/streaming
pathType: Prefix
backend:
service:
name: mastodon-streaming
port:
number: {{ .streamingPort }}
- path: /
pathType: Prefix
backend:
service:
name: mastodon-web
port:
number: {{ .webPort }}

View File

@@ -0,0 +1,21 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: {{ .namespace }}
labels:
- includeSelectors: true
pairs:
app: mastodon
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- pvc-assets.yaml
- pvc-system.yaml
- db-init-job.yaml
- vapid-init-job.yaml
- deployment-web.yaml
- deployment-sidekiq.yaml
- deployment-streaming.yaml
- service-web.yaml
- service-streaming.yaml
- ingress.yaml

67
mastodon/manifest.yaml Normal file
View File

@@ -0,0 +1,67 @@
name: mastodon
is: mastodon
description: Mastodon is a free, open-source social network server based on ActivityPub.
version: 4.5.3
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mastodon.svg
requires:
- name: postgres
- name: redis
defaultConfig:
namespace: mastodon
externalDnsDomain: "{{ .cloud.domain }}"
timezone: UTC
image: ghcr.io/mastodon/mastodon:v4.5.3
streamingImage: ghcr.io/mastodon/mastodon-streaming:v4.5.3
domain: mastodon.{{ .cloud.domain }}
locale: en
singleUserMode: false
# Database configuration
dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbName: mastodon_production
dbUsername: mastodon
# Redis configuration
redisHostname: "{{ .apps.redis.host }}"
redisPort: "{{ .apps.redis.port }}"
# Ports
webPort: 3000
streamingPort: 4000
# Storage
assetsStorage: 10Gi
systemStorage: 100Gi
# SMTP configuration
smtp:
enabled: "{{ .cloud.smtp.host | ternary true false }}"
server: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
from: notifications@{{ .cloud.domain }}
user: "{{ .cloud.smtp.user }}"
authMethod: plain
enableStarttls: auto
tls: "{{ .cloud.smtp.tls }}"
# TLS
tlsSecretName: wildcard-wild-cloud-tls
# Sidekiq configuration
sidekiq:
replicas: 1
concurrency: 25
defaultSecrets:
- key: secretKeyBase
default: "{{ random.AlphaNum 128 }}"
- key: otpSecret
default: "{{ random.AlphaNum 128 }}"
- key: vapidPrivateKey
# Generated by vapid-init-job.yaml on first deploy
- key: vapidPublicKey
# Generated by vapid-init-job.yaml on first deploy
- key: activeRecordPrimaryKey
default: "{{ random.AlphaNum 32 }}"
- key: activeRecordDeterministicKey
default: "{{ random.AlphaNum 32 }}"
- key: activeRecordKeyDerivationSalt
default: "{{ random.AlphaNum 32 }}"
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- postgres.password
- redis.password

4
mastodon/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .namespace }}

11
mastodon/pvc-assets.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mastodon-assets
namespace: {{ .namespace }}
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .assetsStorage }}

11
mastodon/pvc-system.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mastodon-system
namespace: {{ .namespace }}
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .systemStorage }}

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mastodon-streaming
namespace: {{ .namespace }}
spec:
type: ClusterIP
ports:
- port: {{ .streamingPort }}
targetPort: streaming
protocol: TCP
name: streaming
selector:
component: streaming

14
mastodon/service-web.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mastodon-web
namespace: {{ .namespace }}
spec:
type: ClusterIP
ports:
- port: {{ .webPort }}
targetPort: http
protocol: TCP
name: http
selector:
component: web

View File

@@ -0,0 +1,68 @@
apiVersion: batch/v1
kind: Job
metadata:
name: mastodon-vapid-init
namespace: {{ .namespace }}
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
component: vapid-init
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 991
runAsGroup: 991
fsGroup: 991
seccompProfile:
type: RuntimeDefault
containers:
- name: vapid-init
image: {{ .image }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
command:
- sh
- -c
- |
set -e
# Check if VAPID keys already exist in the secret
if [ -n "$VAPID_PRIVATE_KEY" ] && [ "$VAPID_PRIVATE_KEY" != "null" ] && \
[ -n "$VAPID_PUBLIC_KEY" ] && [ "$VAPID_PUBLIC_KEY" != "null" ]; then
echo "VAPID keys already exist in secret, skipping generation"
exit 0
fi
echo "Generating VAPID keys..."
bundle exec rake mastodon:webpush:generate_vapid_key > /tmp/vapid_output.txt
echo "VAPID keys generated:"
cat /tmp/vapid_output.txt
echo ""
echo "NOTE: These keys must be manually added to secrets.yaml:"
echo " apps.mastodon.vapidPrivateKey: <VAPID_PRIVATE_KEY from above>"
echo " apps.mastodon.vapidPublicKey: <VAPID_PUBLIC_KEY from above>"
env:
- name: VAPID_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPrivateKey
optional: true
- name: VAPID_PUBLIC_KEY
valueFrom:
secretKeyRef:
name: mastodon-secrets
key: vapidPublicKey
optional: true
- name: RAILS_ENV
value: production
- name: LOCAL_DOMAIN
value: "{{ .domain }}"

66
matrix/configmap.yaml Normal file
View File

@@ -0,0 +1,66 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: matrix-config
data:
homeserver.yaml: |
server_name: "{{ .serverName }}"
public_baseurl: https://{{ .domain }}
listeners:
- port: {{ .port }}
tls: false
type: http
x_forwarded: true
bind_addresses: ['::']
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: {{ .dbUsername }}
password: ${DB_PASSWORD}
database: {{ .dbName }}
host: {{ .dbHostname }}
port: 5432
cp_min: 5
cp_max: 10
redis:
enabled: true
host: {{ .redisHostname }}
port: 6379
password: ${REDIS_PASSWORD}
media_store_path: /data/media_store
uploads_path: /data/uploads
max_upload_size: 100M
enable_registration: {{ .enableRegistration }}
registration_shared_secret: "${REGISTRATION_SHARED_SECRET}"
macaroon_secret_key: "${MACAROON_SECRET_KEY}"
form_secret: "${FORM_SECRET}"
signing_key_path: /data/keys/signing.key
trusted_key_servers:
- server_name: "matrix.org"
email:
smtp_host: "{{ .smtp.host }}"
smtp_port: {{ .smtp.port }}
smtp_user: "{{ .smtp.user }}"
smtp_pass: "${SMTP_PASSWORD}"
require_transport_security: {{ .smtp.requireTls }}
notif_from: "{{ .smtp.from }}"
app_name: Matrix
report_stats: false
enable_metrics: true
suppress_key_server_warning: true

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