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
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.
@@ -22,12 +22,14 @@ This is the contents of an example `manifest.yaml` file for an app named "immich
```yaml
name: immich
is: immich
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
version: 1.0.0
icon: https://immich.app/assets/images/logo.png
requires:
- name: redis
- name: postgres
- name: pg
alias: db # Use a different reference name in templates
- name: redis # 'alias' and 'installedAs' default to 'name' value
defaultConfig:
serverImage: ghcr.io/immich-app/immich-server:release
mlImage: ghcr.io/immich-app/immich-machine-learning:release
@@ -36,28 +38,195 @@ defaultConfig:
mlPort: 3003
storage: 250Gi
cacheStorage: 10Gi
redisHostname: redis.redis.svc.cluster.local
dbHostname: postgres.postgres.svc.cluster.local
dbUsername: immich
redisHostname: "{{ .apps.redis.host }}" # Can reference 'requires' app configurations
dbHostname: "{{ .apps.pg.host }}"
db: # Configuration can be nested
name: immich
user: immich
host: "{{ .apps.pg.host }}"
port: "{{ .apps.pg.port }}"
domain: immich.{{ .cloud.domain }}
defaultSecrets:
- apps.immich.dbPassword
- apps.postgres.password
- key: password # Random value will be generated if empty
- 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 |
|-------|----------|-------------|
| `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 |
| `version` | Yes | App version (follow upstream versioning) |
| `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` |
| `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`)
@@ -111,21 +280,7 @@ This means individual resources can use simple, component-specific selectors lik
### 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:
```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.
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:
### External DNS
@@ -133,12 +288,47 @@ Ingress resources should include external-dns annotations for automatic DNS mana
```yaml
annotations:
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }}
external-dns.alpha.kubernetes.io/target: {{ .domain }}
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`).
## 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 Initialization Jobs
@@ -211,13 +401,16 @@ spec:
### 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:**
```yaml
defaultSecrets:
- apps.myapp.dbPassword
- apps.postgres.password
key: dbPassword # This app's database 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:**
@@ -227,14 +420,26 @@ env:
valueFrom:
secretKeyRef:
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:**
1. List secrets in manifest's `defaultSecrets`
2. When adding an app, the system generates random values in the instance's `secrets.yaml`
3. When deploying, the system creates a Kubernetes Secret named `<app-name>-secrets`
4. Resources reference secrets using full dotted paths
1. Define app's own secrets in `defaultSecrets` (key, default mappings)
2. Reference dependency secrets in `requiredSecrets` (list)
3. When adding an app, the system:
- 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.
@@ -308,9 +513,11 @@ Before submitting a new or modified app, verify:
- [ ] **Manifest**
- [ ] `name` matches directory name
- [ ] All required fields present (`name`, `description`, `version`, `defaultConfig`)
- [ ] All template variables defined in `defaultConfig` or are standard Wild Cloud variables
- [ ] Secrets use dotted-path format (e.g., `apps.appname.secretname`)
- [ ] Dependencies listed in `requires` (if any)
- [ ] All template variables defined in `defaultConfig`
- [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
- [ ] `requiredSecrets` references use `<app-ref>.<key>` format
- [ ] Dependencies listed in `requires` with optional `alias` fields
- [ ] Manifest template references match dependency aliases or names
- [ ] **Kustomization**
- [ ] 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:`
- [ ] **Resources**
- [ ] All hardcoded values replaced with gomplate variables
- [ ] Secrets reference full dotted paths
- [ ] Security contexts on all pods (both pod-level and container-level)
- [ ] Simple component labels, no Helm-style labels
- [ ] Ingresses include external-dns annotations

View File

@@ -1,15 +1,97 @@
- @README.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
- 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.
- 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 postgres for database if supported.
- 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.
## 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
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
env:
- name: PGHOST
value: "{{ .apps.discourse.dbHostname }}"
value: "{{ .dbHostname }}"
- name: PGPORT
value: "5432"
- name: PGUSER
@@ -36,16 +36,16 @@ spec:
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.postgres.password
key: postgres.password
- name: DISCOURSE_DB_USER
value: "{{ .apps.discourse.dbUsername }}"
value: "{{ .dbUsername }}"
- name: DISCOURSE_DB_NAME
value: "{{ .apps.discourse.dbName }}"
value: "{{ .dbName }}"
- name: DISCOURSE_DB_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.dbPassword
key: dbPassword
command:
- /bin/sh
- -c

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ kind: Ingress
metadata:
name: example-app
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
# Optional: Enable HTTPS redirection
@@ -15,7 +15,7 @@ metadata:
# traefik.ingress.kubernetes.io/auth-secret: basic-auth
spec:
rules:
- host: example-app.{{ .cloud.domain }}
- host: "{{ .host }}"
http:
paths:
- path: /
@@ -27,5 +27,5 @@ spec:
number: 80
tls:
- hosts:
- example-app.{{ .cloud.domain }}
- "{{ .host }}"
secretName: wildcard-wild-cloud-tls

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,12 +7,12 @@ metadata:
kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
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"
traefik.ingress.kubernetes.io/redirect-entry-point: https
spec:
rules:
- host: {{ .apps.ghost.domain }}
- host: {{ .domain }}
http:
paths:
- path: /
@@ -24,5 +24,5 @@ spec:
number: 80
tls:
- hosts:
- {{ .apps.ghost.domain }}
secretName: {{ .apps.ghost.tlsSecretName }}
- {{ .domain }}
secretName: {{ .tlsSecretName }}

View File

@@ -1,10 +1,14 @@
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
icon: https://ghost.org/images/logos/ghost-logo-orb.png
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
requires:
- name: mysql
- name: mysql
defaultConfig:
namespace: ghost
externalDnsDomain: '{{ .cloud.domain }}'
image: docker.io/bitnami/ghost:5.118.1-debian-12-r0
domain: ghost.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
@@ -15,16 +19,17 @@ defaultConfig:
dbName: ghost
dbUser: ghost
adminUser: admin
adminEmail: "admin@{{ .cloud.domain }}"
blogTitle: "My Blog"
adminEmail: {{ .operator.email }}
blogTitle: My Blog
timezone: UTC
tlsSecretName: wildcard-wild-cloud-tls
smtp:
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
from: "{{ .cloud.smtp.from }}"
user: "{{ .cloud.smtp.user }}"
host: '{{ .cloud.smtp.host }}'
port: '{{ .cloud.smtp.port }}'
from: '{{ .cloud.smtp.from }}'
user: '{{ .cloud.smtp.user }}'
defaultSecrets:
- key: apps.ghost.adminPassword
- key: apps.ghost.dbPassword
- key: apps.ghost.smtpPassword
- key: adminPassword
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- mysql.rootPassword

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,13 @@
name: gitea
is: gitea
description: Gitea is a painless self-hosted Git service written in Go
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:
- name: postgres
- name: postgres
defaultConfig:
namespace: gitea
externalDnsDomain: '{{ .cloud.domain }}'
image: gitea/gitea:1.24.3
appName: Gitea
domain: gitea.{{ .cloud.domain }}
@@ -16,18 +19,20 @@ defaultConfig:
dbUser: gitea
dbHost: postgres.postgres.svc.cluster.local
adminUser: admin
adminEmail: "admin@{{ .cloud.domain }}"
adminEmail: "{{ .operator.email }}"
dbPort: 5432
timezone: UTC
runMode: prod
smtp:
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
from: "{{ .cloud.smtp.from }}"
host: '{{ .cloud.smtp.host }}'
port: '{{ .cloud.smtp.port }}'
user: '{{ .cloud.smtp.user }}'
from: '{{ .cloud.smtp.from }}'
defaultSecrets:
- key: apps.gitea.adminPassword
- key: apps.gitea.dbPassword
- key: apps.gitea.secretKey
- key: apps.gitea.jwtSecret
- key: apps.gitea.smtpPassword
- key: adminPassword
- key: dbPassword
- key: secretKey
- key: jwtSecret
- key: smtpPassword
requiredSecrets:
- postgres.password

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,12 +5,12 @@ metadata:
annotations:
traefik.ingress.kubernetes.io/router.tls: "true"
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"
traefik.ingress.kubernetes.io/router.middlewares: keila-cors@kubernetescrd
spec:
rules:
- host: {{ .apps.keila.domain }}
- host: {{ .domain }}
http:
paths:
- path: /
@@ -23,4 +23,4 @@ spec:
tls:
- secretName: "wildcard-wild-cloud-tls"
hosts:
- "{{ .apps.keila.domain }}"
- "{{ .domain }}"

View File

@@ -1,15 +1,19 @@
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.
version: 1.0.0
icon: https://www.keila.io/images/logo.svg
version: 0.17.1
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg
requires:
- name: postgres
defaultConfig:
image: pentacent/keila:latest
namespace: keila
externalDnsDomain: "{{ .cloud.domain }}"
image: pentacent/keila:0.17.1
port: 4000
storage: 1Gi
domain: keila.{{ .cloud.domain }}
dbHostname: postgres.postgres.svc.cluster.local
dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbName: keila
dbUsername: keila
disableRegistration: "true"
@@ -20,12 +24,15 @@ defaultConfig:
port: "{{ .cloud.smtp.port }}"
from: "{{ .cloud.smtp.from }}"
user: "{{ .cloud.smtp.user }}"
tls: {{ .cloud.smtp.tls }}
startTls: {{ .cloud.smtp.startTls }}
tls: "{{ .cloud.smtp.tls }}"
startTls: "{{ .cloud.smtp.startTls }}"
defaultSecrets:
- key: apps.keila.secretKeyBase
- key: apps.keila.dbPassword
- key: apps.keila.dbUrl
- key: apps.keila.adminPassword
- key: apps.keila.smtpPassword
- key: apps.postgres.password
- key: secretKeyBase
default: "{{ random.AlphaNum 64 }}"
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/keila?sslmode=disable"
- key: adminPassword
- key: smtpPassword
requiredSecrets:
- postgres.password

View File

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

View File

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

View File

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

View File

@@ -7,5 +7,5 @@ spec:
component: web
ports:
- port: 80
targetPort: {{ .apps.keila.port }}
targetPort: {{ .port }}
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
env:
- name: PGHOST
value: {{ .apps.listmonk.dbHost }}
value: {{ .dbHost }}
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: apps.postgres.password
key: postgres.password
- name: DB_NAME
value: {{ .apps.listmonk.dbName }}
value: {{ .dbName }}
- name: DB_USER
value: {{ .apps.listmonk.dbUser }}
value: {{ .dbUser }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: apps.listmonk.dbPassword
key: dbPassword
command:
- /bin/bash
- -c

View File

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

View File

@@ -6,16 +6,16 @@ metadata:
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
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"
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .apps.listmonk.domain }}
secretName: {{ .apps.listmonk.tlsSecretName }}
- {{ .domain }}
secretName: {{ .tlsSecretName }}
rules:
- host: {{ .apps.listmonk.domain }}
- host: {{ .domain }}
http:
paths:
- path: /

View File

@@ -1,11 +1,16 @@
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
icon: https://listmonk.app/static/images/logo.svg
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/listmonk.svg
requires:
- name: postgres
- name: postgres
defaultConfig:
namespace: listmonk
externalDnsDomain: '{{ .cloud.domain }}'
domain: listmonk.{{ .cloud.domain }}
rootUrl: https://listmonk.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
dbHost: postgres.postgres.svc.cluster.local
@@ -15,6 +20,8 @@ defaultConfig:
dbSSLMode: disable
timezone: UTC
defaultSecrets:
- key: apps.listmonk.dbPassword
- key: apps.listmonk.dbUrl
- key: apps.postgres.password
- key: dbPassword
- key: dbUrl
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
kind: Namespace
metadata:
name: listmonk
name: "{{ .namespace }}"

View File

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

View File

@@ -8,57 +8,48 @@ spec:
restartPolicy: OnFailure
containers:
- 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:
- name: PGHOST
value: "{{ .db.host }}"
- name: PGPORT
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
- name: RAILS_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: loomio-secrets
key: dbPassword
command:
- sh
- -c
- |
echo "Creating database and user for Loomio..."
# Check if database exists, create if not
psql -tc "SELECT 1 FROM pg_database WHERE datname = '$LOOMIO_DB_NAME'" | grep -q 1 || \
psql -c "CREATE DATABASE \"$LOOMIO_DB_NAME\""
# Check if user exists, create or update password
psql -tc "SELECT 1 FROM pg_user WHERE usename = '$LOOMIO_DB_USER'" | grep -q 1 && \
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!"
key: dbUrl
- name: REDIS_URL
value: {{ .redisUrl }}
- name: DEVISE_SECRET
valueFrom:
secretKeyRef:
name: loomio-secrets
key: deviseSecret
- name: SECRET_COOKIE_TOKEN
valueFrom:
secretKeyRef:
name: loomio-secrets
key: secretCookieToken
securityContext:
runAsNonRoot: true
runAsUser: 999 # postgres user
runAsGroup: 999
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
type: RuntimeDefault
securityContext:
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault

View File

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

View File

@@ -15,6 +15,13 @@ spec:
containers:
- name: loomio
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:
- containerPort: 3000
name: http
@@ -73,10 +80,12 @@ spec:
secretKeyRef:
name: loomio-secrets
key: smtpPassword
- name: SMTP_USE_SSL
value: "{{ .smtp.tls }}"
- name: REPLY_HOSTNAME
value: {{ .smtp.from }}
- name: CHANNELS_URI
value: wss://{{ .domain }}
- name: BUNDLE_APP_CONFIG
value: /loomio/tmp/.bundle
volumeMounts:
- name: uploads
mountPath: /loomio/public/system
@@ -84,6 +93,8 @@ spec:
mountPath: /loomio/storage
- name: tmp
mountPath: /loomio/tmp
- name: log
mountPath: /loomio/log
resources:
requests:
memory: 512Mi
@@ -92,21 +103,18 @@ spec:
memory: 2Gi
cpu: 1000m
livenessProbe:
httpGet:
path: /health
tcpSocket:
port: 3000
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
tcpSocket:
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
@@ -121,4 +129,6 @@ spec:
persistentVolumeClaim:
claimName: loomio-storage
- name: tmp
emptyDir: {}
- name: log
emptyDir: {}

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ metadata:
name: loomio-uploads
spec:
accessModes:
- ReadWriteOnce
- ReadWriteMany
resources:
requests:
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