Compare commits

...

25 Commits

Author SHA1 Message Date
Paul Payne
9b0c56f720 mailu app initial attempt 2026-02-15 18:30:39 +00:00
Paul Payne
ebc19a9595 upgrade(open-webui): Update to v0.8.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 05:23:31 +00:00
Paul Payne
3a40c7266a Update open-webui deployment and manifest for version 0.7.2; streamline configuration variables 2026-02-13 05:19:56 +00:00
Paul Payne
78adc2883f Fix DNS configuration variable names in ADDING-APPS.md 2026-02-01 11:09:16 +00:00
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
Paul Payne
8818d822cf loomio (not yet working), and new config for postgres and redis 2025-12-30 03:39:19 +00:00
Paul Payne
1b78abbdc4 claude 2025-12-30 03:38:48 +00:00
Paul Payne
a4db0d0f6a change to defaultSecrets 2025-12-30 03:38:39 +00:00
Paul Payne
351f58b80d Update docs for adding apps. 2025-10-22 22:36:53 +00:00
Paul Payne
458e70b7be mysql connection improvement. 2025-10-18 19:01:37 +00:00
Paul Payne
f68ff43879 Updates documentation. 2025-10-18 18:57:35 +00:00
Paul Payne
47c23d3f1b Updates mysql to not use bitnami. 2025-10-18 18:57:22 +00:00
169 changed files with 4942 additions and 1033 deletions

View File

@@ -1,27 +1,35 @@
# Adding custom apps
# Adding Wild Cloud Apps
Custom apps can be added to your Wild Cloud apps directory.
This guide is for contributors and maintainers who want to create or modify Wild Cloud apps. If you're looking to use existing apps, see [README.md](README.md).
Custom apps can be deployed using Wild Cloud scripts. Wild Cloud apps follow a specific structure and naming convention to ensure compatibility with the Wild Cloud ecosystem.
## Overview
## App Structure
Wild Cloud apps are Kubernetes applications packaged as Kustomize configurations with standardized conventions for configuration management, secrets handling, and deployment.
Each subdirectory in this directory represents a Wild Cloud app. Each app directory contains an "app manifest" (`manifest.yaml`), a "kustomization" (`kustomization.yaml`), and one or more "configurations" (yaml files containing definitions/configurations of Kubernetes objects/resources).
## Required Files
### App Manifest
Each app directory must contain:
The required `manifest.yaml` file contains metadata about the app.
1. **`manifest.yaml`** - App metadata and configuration schema
2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.)
## App Manifest (`manifest.yaml`)
The manifest defines the app's metadata, dependencies, configuration schema, and secret requirements.
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
@@ -30,30 +38,199 @@ 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:
- 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:
- apps.immich.dbPassword
- apps.postgres.password
- db.password # References postgres app via 'db' alias
- redis.auth # References redis app via 'redis' name (no alias)
```
Explanation of the fields:
### Manifest Fields
- `name`: The name of the app, used for identification.
- `description`: A brief description of the app.
- `version`: The version of the app. This should generally follow the versioning scheme of the app itself.
- `icon`: A URL to an icon representing the app.
- `requires`: A list of other apps that this app depends on. Each entry should be the name of another app.
- `defaultConfig`: A set of default configuration values for the app. When an app is added using `wild-app-add`, these values will be added to the Wild Cloud `config.yaml` file.
- `requiredSecrets`: A list of secrets that must be set in the Wild Cloud `secrets.yaml` file for the app to function properly. These secrets are typically sensitive information like database passwords or API keys. Keys with random values will be generated automatically when the app is added.
| 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 with optional aliases |
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
| `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) |
### Kustomization
### Dependency Configuration
Each app directory should also contain a `kustomization.yaml` file. This file defines how the app's Kubernetes resources are built and deployed. It can include references to other Kustomize files, patches, and configurations.
- 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`)
Here is an example `kustomization.yaml` file for the "immich" app:
### 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.dnsmasq.ip - IP address of the DNS server (Wild Central)
- cluster.internalDns.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`)
The kustomization file defines how Kubernetes resources are built and applies Wild Cloud's standard labels.
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
@@ -74,105 +251,129 @@ resources:
- pvc.yaml
- service.yaml
- db-init-job.yaml
```
```
Kustomization requirements:
#### Kustomization Requirements
- Every Wild Cloud kustomization should include the Wild Cloud labels in its `kustomization.yaml` file. This allows the Wild Cloud to identify and manage the app correctly. The labels should be defined under the `labels` key, as shown in the example above.
- The `app` label and `namespace` keys should the app's name/directory.
- **Namespace**: Must match the app name
- **Labels**: Must include standard Wild Cloud labels with `includeSelectors: true`
- **Resources**: List all Kubernetes manifest files
#### Standard Wild Cloud Labels
#### Labeling Strategy
Wild Cloud uses a consistent labeling strategy across all apps:
Wild Cloud uses Kustomize's `includeSelectors: true` feature to automatically apply standard labels to all resources AND their selectors:
```yaml
labels:
- includeSelectors: true
pairs:
app: myapp # The app name (matches directory)
managedBy: kustomize # Managed by Kustomize
partOf: wild-cloud # Part of Wild Cloud ecosystem
app: myapp # App name (matches directory)
managedBy: kustomize
partOf: wild-cloud
```
The `includeSelectors: true` setting automatically applies these labels to all resources AND their selectors, which means:
This means individual resources can use simple, component-specific selectors like `component: web`, and Kustomize will automatically expand them to include all Wild Cloud labels.
1. **Resource labels** - All resources get the standard Wild Cloud labels
2. **Selector labels** - All selectors automatically include these labels for robust selection
**Do NOT use Helm-style labels** (`app.kubernetes.io/name`, `app.kubernetes.io/instance`). Use simple component labels (`component: web`, `component: worker`, etc.) instead.
This allows individual resources to use simple, component-specific selectors:
## Configuration Templates
### Gomplate Templating
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
Ingress resources should include external-dns annotations for automatic DNS management:
```yaml
selector:
matchLabels:
component: web
annotations:
external-dns.alpha.kubernetes.io/target: {{ .domain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
```
Which Kustomize automatically expands to:
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
selector:
matchLabels:
app: myapp
component: web
managedBy: kustomize
partOf: wild-cloud
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
```
### Configuration Files
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
Wild Cloud apps use Kustomize as kustomize files are simple, transparent, and easier to manage in a Git repository.
### Example: Multiple Database Instances
#### Templates
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
```
For operators, Wild Cloud apps use standard configuration files. This makes modifying the app's configuration straightforward, as operators can customize their app files as needed. They can choose to manage modifications and updates directly on the configuration files using `git` tools, or they can use Kustomize patches or overlays. As a convenience for operators, when adding an app (using `wild-app-add`), the app's configurations will be compiled with the operator's Wild Cloud configuration and secrets. This results in standard Kustomize files being placed in the Wild Cloud home directory, which can then be modified as needed. This means the configuration files in this repository are actually templates, but they will be compiled into standard Kustomize files when the app is added to an operator's Wild Cloud home directory.
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.
To reference operator configuration in the configuration files, use gomplate variables, such as `{{ .cloud.domain }}` for the domain name. All configuration variables you use need to exist in the operator's `config.yaml`, so they should be either standard Wild Cloud operator variables, or be defined in the app's `manifest.yaml` under `defaultConfig`.
## Database Patterns
When `wild-app-add` is run, the app's Kustomize files will be compiled with the operator's Wild Cloud configuration and secrets resulting in standard Kustomize files being placed in the Wild Cloud home directory.
### Database Initialization Jobs
#### External DNS Configuration
Apps requiring PostgreSQL or MySQL should include a database initialization job (`db-init-job.yaml`):
Wild Cloud apps use external-dns annotations in their ingress resources to automatically manage DNS records:
**Purpose:**
- Creates the application database (if it doesn't exist)
- Creates/updates the application user with proper credentials
- Grants necessary permissions
- Installs required database extensions (e.g., PostgreSQL's `vector`, `cube`, `earthdistance`)
- `external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }}` - Creates a CNAME record pointing the app subdomain to the main cluster domain (e.g., `ghost.cloud.payne.io``cloud.payne.io`)
- `external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"` - Disables Cloudflare proxy for direct DNS resolution
**Implementation requirements:**
- Use `restartPolicy: OnFailure`
- Include in `kustomization.yaml` resources
- Use appropriate security context (e.g., `runAsUser: 999` for PostgreSQL)
#### Database Initialization Jobs
**Example apps:** `immich`, `gitea`, `openproject`, `discourse`
Apps that rely on PostgreSQL or MySQL databases typically need a database initialization job to create the required database and user before the main application starts. These jobs:
### Database URL Configuration
- Run as Kubernetes Jobs that execute once and complete
- Create the application database if it doesn't exist
- Create the application user with appropriate permissions
- Should be included in the app's `kustomization.yaml` resources list
- Use the same database connection settings as the main application
When apps need database URLs with embedded credentials, **always use a dedicated `dbUrl` secret**.
Examples of apps with db-init jobs: `gitea`, `codimd`, `immich`, `openproject`
##### Database URL Configuration
**Important:** When apps require database URLs with embedded credentials, always use a separate `dbUrl` secret instead of trying to construct the URL with environment variable substitution in Kustomize templates.
**Wrong** (Kustomize cannot process runtime env var substitution):
**Wrong** - Kustomize cannot process runtime env var substitution:
```yaml
- name: DB_URL
value: "postgresql://user:$(DB_PASSWORD)@host/db"
value: "postgresql://user:$(DB_PASSWORD)@host/db" # This won't work!
```
**Correct** (Use a dedicated secret):
**Correct** - Use a dedicated secret:
```yaml
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: apps.appname.dbUrl
name: myapp-secrets
key: apps.myapp.dbUrl
```
Add `apps.appname.dbUrl` to the manifest's `requiredSecrets` and the `wild-app-add` script will generate the complete URL with embedded credentials.
Add `apps.myapp.dbUrl` to your manifest's `defaultSecrets`, and the system will generate the complete URL with embedded credentials automatically when the app is added.
##### Security Context Requirements
## Security Requirements
Pods must comply with Pod Security Standards. All pods should include proper security contexts to avoid deployment warnings:
### Security Contexts
**All pods must comply with Pod Security Standards.** Include security contexts at both pod and container levels:
```yaml
spec:
@@ -180,8 +381,8 @@ spec:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999 # Use appropriate non-root user ID
runAsGroup: 999 # Use appropriate group ID
runAsUser: 999 # Use appropriate non-root UID
runAsGroup: 999 # Use appropriate GID
seccompProfile:
type: RuntimeDefault
containers:
@@ -189,77 +390,162 @@ spec:
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
drop: [ALL]
readOnlyRootFilesystem: false # Set to true when possible
```
For PostgreSQL init jobs, use `runAsUser: 999` (postgres user). For other database types, use the appropriate non-root user ID for that database container.
**Common user IDs:**
- PostgreSQL: `runAsUser: 999`
- Redis: `runAsUser: 999`
- MySQL: Consult the container image documentation
#### Secrets
### Secrets Management
Secrets are managed in the `secrets.yaml` file in the Wild Cloud home directory. The app's `manifest.yaml` should list any required secrets under `requiredSecrets`. When the app is added, default secret values will be generated and stored in the `secrets.yaml` file. Secrets are always stored and referenced in the `apps.<app-name>.<secret-name>` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the name `<app-name>-secrets`, containing all secrets defined in the manifest's `requiredSecrets` key. These secrets can then be referenced in the app's Kustomize files using a `secretKeyRef`.
**Important:** Always use the full dotted path from the manifest as the secret key, not just the last segment. For example, to mount a secret in an environment variable, you would use:
Secrets are managed through two mechanisms: default secrets for the app itself and required secrets from dependencies.
**In manifest:**
```yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: immich-secrets
key: apps.immich.dbPassword # Use full dotted path, not just "dbPassword"
defaultSecrets:
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
```
This approach prevents naming conflicts between apps and makes secret keys more descriptive and consistent with the `secrets.yaml` structure.
**In resources:**
```yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: dbPassword # Points to the default secret
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db.password # Points to the required secret
```
`secrets.yaml` files should not be checked in to a git repository and are ignored by default in Wild Cloud home directories. Checked in kustomize files should only reference secrets, not compile them.
**Secret workflow:**
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>`
## App Lifecycle
**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.
Apps in Wild Cloud are managed by operators using a set of commands run from their Wild Cloud home directory.
**Important:** Never commit `secrets.yaml` to Git. Templates should only reference secrets, never contain actual secret values.
- `wild-apps-list`: Lists all available apps.
- `wild-app-add <app-name>`: Reads the app from the Wild Cloud repository, adds the app manifest to your Wild Cloud home `apps` directory, updates missing values in `config.yaml` and `secrets.yaml` with the app's default configurations, and compiles the app's Kustomize files.
- `wild-app-deploy <app-name>`: Deploys the app to your Wild Cloud.
## Converting from Helm Charts
## Contributing
Wild Cloud prefers Kustomize over Helm for simplicity and Git-friendliness. When an official Helm chart exists, convert it rather than creating manifests from scratch.
If you would like to contribute an app to the Wild Cloud, issue a pull request with the app's directory containing the `manifest.yaml` file and any necessary Kustomize files. Ensure that your app follows the structure outlined above.
## Tips for App Packagers
### Converting from Helm Charts
Wild Cloud apps use Kustomize as kustomize files are simpler, more transparent, and easier to manage in a Git repository than Helm charts.
IMPORTANT! If an official Helm chart is available for an app, it is recommended to convert that chart to a Wild Cloud app rather than creating a new app from scratch.
If you have a Helm chart that you want to convert to a Wild Cloud app, the following example steps can simplify the process for you:
### Conversion Process
1. **Extract and render the Helm chart:**
```bash
helm fetch --untar --untardir charts nginx-stable/nginx-ingress
helm template --output-dir base --namespace ingress --values values.yaml ingress-controller charts/nginx-ingress
cat <<EOF > base/nginx-ingress/namespace.yaml
helm fetch --untar --untardir charts repo/chart-name
helm template --output-dir base --namespace myapp --values values.yaml myapp charts/chart-name
cd base/chart-name
```
2. **Add namespace manifest:**
```bash
cat <<EOF > namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: ingress
name: myapp
EOF
cd base/nginx-ingress
```
3. **Create kustomization:**
```bash
kustomize create --autodetect
```
After running these commands against your own Helm chart, you will have a Kustomize directory structure that can be used as a Wild Cloud app. All you need to do then, usually, is:
4. **Convert to Wild Cloud format:**
- Create `manifest.yaml` with app metadata
- Replace hardcoded values with gomplate variables (e.g., `{{ .cloud.domain }}`)
- Update secrets to use dotted-path convention
- Replace Helm labels with Wild Cloud standard labels
- Add `includeSelectors: true` to kustomization
- Use simple component labels (`component: web`, not `app.kubernetes.io/name`)
- Add security contexts to all pods
- Add external-dns annotations to ingresses
- add an app manifest (a `manifest.yaml` file).
- replace any hardcoded operator values with Wild Cloud operator variables, such as `{{ .cloud.domain }}` for the domain name.
- modify how secrets are referenced in the Kustomize files (see above)
- update labels and selectors to use the Wild Cloud standard:
- Replace complex Helm labels (like `app.kubernetes.io/name`, `app.kubernetes.io/instance`) with simple component labels
- Use `component: web`, `component: worker`, etc. in selectors and pod template labels
- Let Kustomize handle the common labels (`app`, `managedBy`, `partOf`) automatically
- remove any Helm-specific labels from the Kustomize files, as Wild Cloud apps do not use Helm labels.
### Example Label Migration
**Helm style:**
```yaml
labels:
app.kubernetes.io/name: myapp
app.kubernetes.io/instance: release-name
app.kubernetes.io/component: server
```
**Wild Cloud style:**
```yaml
# In kustomization.yaml (applied automatically)
labels:
- includeSelectors: true
pairs:
app: myapp
managedBy: kustomize
partOf: wild-cloud
# In individual resources
labels:
component: server # Simple component label
```
## Validation Checklist
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`
- [ ] `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`
- [ ] Namespace matches app name
- [ ] All resource files listed under `resources:`
- [ ] **Resources**
- [ ] Security contexts on all pods (both pod-level and container-level)
- [ ] Simple component labels, no Helm-style labels
- [ ] Ingresses include external-dns annotations
- [ ] Database apps include init jobs (if applicable)
- [ ] **Testing**
- [ ] Templates compile successfully with sample config
- [ ] App deploys without errors in test cluster
- [ ] All dependencies work correctly
## Contributing
Contributions are welcome! To contribute:
1. Fork the repository
2. Create a new app directory following the structure above
3. Test your app thoroughly
4. Submit a pull request with:
- Description of the app and its purpose
- Any special configuration notes
- Dependencies required
## Notice: Third-Party Software

97
CLAUDE.md Normal file
View File

@@ -0,0 +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.

310
README.md
View File

@@ -1,265 +1,107 @@
# Wild Cloud-maintained apps
# Wild Cloud Apps Directory
This is the Wild Cloud apps repository.
This is the official Wild Cloud apps repository containing a curated collection of self-hosted applications that can be deployed to your Wild Cloud cluster.
This repository contains a collection of apps that can be deployed using Wild Cloud scripts. Wild Cloud apps follow a specific structure and naming convention to ensure compatibility with the Wild Cloud ecosystem.
## What are Wild Cloud Apps?
## App Structure
Wild Cloud apps are pre-packaged Kubernetes applications using Kustomize that follow standardized conventions for configuration, secrets management, and deployment. Each app includes:
Each subdirectory in this directory represents a Wild Cloud app. Each app directory contains an "app manifest" (`manifest.yaml`), a "kustomization" (`kustomization.yaml`), and one or more "configurations" (yaml files containing definitions/configurations of Kubernetes objects/resources).
- **App manifest** (`manifest.yaml`) - Metadata, dependencies, and default configuration
- **Kustomization** (`kustomization.yaml`) - Kubernetes resource definitions
- **Configuration templates** - Deployments, services, ingresses, and other resources
### App Manifest
Apps use gomplate templates that compile with your Wild Cloud configuration when added, making them easy to customize while maintaining a consistent deployment experience.
The required `manifest.yaml` file contains metadata about the app.
## Using Wild Cloud Apps
This is the contents of an example `manifest.yaml` file for an app named "immich":
### Web App (Recommended)
```yaml
name: 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
defaultConfig:
serverImage: ghcr.io/immich-app/immich-server:release
mlImage: ghcr.io/immich-app/immich-machine-learning:release
timezone: UTC
serverPort: 2283
mlPort: 3003
storage: 250Gi
cacheStorage: 10Gi
redisHostname: redis.redis.svc.cluster.local
dbHostname: postgres.postgres.svc.cluster.local
dbUsername: immich
domain: immich.{{ .cloud.domain }}
requiredSecrets:
- apps.immich.dbPassword
- apps.postgres.password
The easiest way to manage apps is through the Wild Cloud web app:
1. **Navigate to Apps**: Access your instance in the web app and go to the Apps page
2. **Browse Available Apps**: View all available apps from the Wild Directory with descriptions and icons
3. **Add an App**: Click on an app to view details and click "Add" to:
- Copy the app manifest to your instance's `apps` directory
- Add default configuration to your `config.yaml`
- Generate required secrets in your `secrets.yaml`
- Compile templates with your configuration
4. **Configure** (optional): Modify app settings before deployment
5. **Deploy**: Click "Deploy" to apply the app to your cluster
6. **Monitor**: View app status, logs, and manage deployments
### CLI
For terminal-based workflows, use the Wild CLI:
```bash
# List available apps
wild app list
# Add app to instance
wild app add <app-name>
# Deploy app
wild app deploy <app-name>
# List deployed apps
wild app list-deployed
# Get app status
wild app status <app-name>
# Delete app
wild app delete <app-name>
```
Explanation of the fields:
The CLI connects to the Wild Central API (default: `http://localhost:5055`). You can override with `--daemon-url` or set `WILD_API_URI` environment variable.
- `name`: The name of the app, used for identification.
- `description`: A brief description of the app.
- `version`: The version of the app. This should generally follow the versioning scheme of the app itself.
- `icon`: A URL to an icon representing the app.
- `requires`: A list of other apps that this app depends on. Each entry should be the name of another app.
- `defaultConfig`: A set of default configuration values for the app. When an app is added using `wild-app-add`, these values will be added to the Wild Cloud `config.yaml` file.
- `requiredSecrets`: A list of secrets that must be set in the Wild Cloud `secrets.yaml` file for the app to function properly. These secrets are typically sensitive information like database passwords or API keys. Keys with random values will be generated automatically when the app is added.
### API
### Kustomization
For automation or advanced workflows, use the Wild Central API:
Each app directory should also contain a `kustomization.yaml` file. This file defines how the app's Kubernetes resources are built and deployed. It can include references to other Kustomize files, patches, and configurations.
```bash
# List available apps
curl http://localhost:5055/api/v1/apps/available
Here is an example `kustomization.yaml` file for the "immich" app:
# Get app details
curl http://localhost:5055/api/v1/apps/available/{app-name}
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: immich
labels:
- includeSelectors: true
pairs:
app: immich
managedBy: kustomize
partOf: wild-cloud
resources:
- deployment-server.yaml
- deployment-machine-learning.yaml
- deployment-microservices.yaml
- ingress.yaml
- namespace.yaml
- pvc.yaml
- service.yaml
- db-init-job.yaml
```
# List deployed apps for an instance
curl http://localhost:5055/api/v1/instances/{instance-name}/apps
Kustomization requirements:
# Add app to configuration
curl -X POST http://localhost:5055/api/v1/instances/{instance-name}/apps \
-H "Content-Type: application/json" \
-d '{"name": "app-name", "config": {}}'
- Every Wild Cloud kustomization should include the Wild Cloud labels in its `kustomization.yaml` file. This allows the Wild Cloud to identify and manage the app correctly. The labels should be defined under the `labels` key, as shown in the example above.
- The `app` label and `namespace` keys should the app's name/directory.
# Deploy app
curl -X POST http://localhost:5055/api/v1/instances/{instance-name}/apps/{app-name}/deploy
#### Standard Wild Cloud Labels
Wild Cloud uses a consistent labeling strategy across all apps:
```yaml
labels:
- includeSelectors: true
pairs:
app: myapp # The app name (matches directory)
managedBy: kustomize # Managed by Kustomize
partOf: wild-cloud # Part of Wild Cloud ecosystem
# Get app status
curl http://localhost:5055/api/v1/instances/{instance-name}/apps/{app-name}/status
```
The `includeSelectors: true` setting automatically applies these labels to all resources AND their selectors, which means:
### How It Works
1. **Resource labels** - All resources get the standard Wild Cloud labels
2. **Selector labels** - All selectors automatically include these labels for robust selection
1. **Add an app**: The system compiles the app's templates using your Wild Cloud configuration (domain, email, etc.) and creates standard Kustomize files in your instance directory
2. **Customize** (optional): Modify app configuration through the web app, CLI, or by editing files directly
3. **Deploy**: The system applies the Kustomize configuration to your cluster
4. **Manage**: Track changes with Git, update configurations, and monitor app health
This allows individual resources to use simple, component-specific selectors:
### Dependencies
```yaml
selector:
matchLabels:
component: web
```
Some apps require other apps to function. For example:
- **Immich** requires PostgreSQL and Redis
- **OpenProject** requires PostgreSQL and Memcached
- **Gitea** requires PostgreSQL
Which Kustomize automatically expands to:
```yaml
selector:
matchLabels:
app: myapp
component: web
managedBy: kustomize
partOf: wild-cloud
```
### Configuration Files
Wild Cloud apps use Kustomize as kustomize files are simple, transparent, and easier to manage in a Git repository.
#### Templates
For operators, Wild Cloud apps use standard configuration files. This makes modifying the app's configuration straightforward, as operators can customize their app files as needed. They can choose to manage modifications and updates directly on the configuration files using `git` tools, or they can use Kustomize patches or overlays. As a convenience for operators, when adding an app (using `wild-app-add`), the app's configurations will be compiled with the operator's Wild Cloud configuration and secrets. This results in standard Kustomize files being placed in the Wild Cloud home directory, which can then be modified as needed. This means the configuration files in this repository are actually templates, but they will be compiled into standard Kustomize files when the app is added to an operator's Wild Cloud home directory.
To reference operator configuration in the configuration files, use gomplate variables, such as `{{ .cloud.domain }}` for the domain name. All configuration variables you use need to exist in the operator's `config.yaml`, so they should be either standard Wild Cloud operator variables, or be defined in the app's `manifest.yaml` under `defaultConfig`.
When `wild-app-add` is run, the app's Kustomize files will be compiled with the operator's Wild Cloud configuration and secrets resulting in standard Kustomize files being placed in the Wild Cloud home directory.
#### External DNS Configuration
Wild Cloud apps use external-dns annotations in their ingress resources to automatically manage DNS records:
- `external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }}` - Creates a CNAME record pointing the app subdomain to the main cluster domain (e.g., `ghost.cloud.payne.io``cloud.payne.io`)
- `external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"` - Disables Cloudflare proxy for direct DNS resolution
#### Database Initialization Jobs
Apps that rely on PostgreSQL or MySQL databases typically need a database initialization job to create the required database and user before the main application starts. These jobs:
- Run as Kubernetes Jobs that execute once and complete
- Create the application database if it doesn't exist
- Create the application user with appropriate permissions
- Should be included in the app's `kustomization.yaml` resources list
- Use the same database connection settings as the main application
Examples of apps with db-init jobs: `gitea`, `codimd`, `immich`, `openproject`
##### Database URL Configuration
**Important:** When apps require database URLs with embedded credentials, always use a separate `dbUrl` secret instead of trying to construct the URL with environment variable substitution in Kustomize templates.
**Wrong** (Kustomize cannot process runtime env var substitution):
```yaml
- name: DB_URL
value: "postgresql://user:$(DB_PASSWORD)@host/db"
```
**Correct** (Use a dedicated secret):
```yaml
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: apps.appname.dbUrl
```
Add `apps.appname.dbUrl` to the manifest's `requiredSecrets` and the `wild-app-add` script will generate the complete URL with embedded credentials.
##### Security Context Requirements
Pods must comply with Pod Security Standards. All pods should include proper security contexts to avoid deployment warnings:
```yaml
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999 # Use appropriate non-root user ID
runAsGroup: 999 # Use appropriate group ID
seccompProfile:
type: RuntimeDefault
containers:
- name: container-name
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false # Set to true when possible
```
For PostgreSQL init jobs, use `runAsUser: 999` (postgres user). For other database types, use the appropriate non-root user ID for that database container.
#### Secrets
Secrets are managed in the `secrets.yaml` file in the Wild Cloud home directory. The app's `manifest.yaml` should list any required secrets under `requiredSecrets`. When the app is added, default secret values will be generated and stored in the `secrets.yaml` file. Secrets are always stored and referenced in the `apps.<app-name>.<secret-name>` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the name `<app-name>-secrets`, containing all secrets defined in the manifest's `requiredSecrets` key. These secrets can then be referenced in the app's Kustomize files using a `secretKeyRef`.
**Important:** Always use the full dotted path from the manifest as the secret key, not just the last segment. For example, to mount a secret in an environment variable, you would use:
```yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: immich-secrets
key: apps.immich.dbPassword # Use full dotted path, not just "dbPassword"
```
This approach prevents naming conflicts between apps and makes secret keys more descriptive and consistent with the `secrets.yaml` structure.
`secrets.yaml` files should not be checked in to a git repository and are ignored by default in Wild Cloud home directories. Checked in kustomize files should only reference secrets, not compile them.
## App Lifecycle
Apps in Wild Cloud are managed by operators using a set of commands run from their Wild Cloud home directory.
- `wild-apps-list`: Lists all available apps.
- `wild-app-add <app-name>`: Reads the app from the Wild Cloud repository, adds the app manifest to your Wild Cloud home `apps` directory, updates missing values in `config.yaml` and `secrets.yaml` with the app's default configurations, and compiles the app's Kustomize files.
- `wild-app-deploy <app-name>`: Deploys the app to your Wild Cloud.
When you add an app, check its `requires` field in the manifest and ensure dependencies are added first.
## Contributing
If you would like to contribute an app to the Wild Cloud, issue a pull request with the app's directory containing the `manifest.yaml` file and any necessary Kustomize files. Ensure that your app follows the structure outlined above.
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.
## Tips for App Packagers
### Converting from Helm Charts
Wild Cloud apps use Kustomize as kustomize files are simpler, more transparent, and easier to manage in a Git repository than Helm charts.
IMPORTANT! If an official Helm chart is available for an app, it is recommended to convert that chart to a Wild Cloud app rather than creating a new app from scratch.
If you have a Helm chart that you want to convert to a Wild Cloud app, the following example steps can simplify the process for you:
```bash
helm fetch --untar --untardir charts nginx-stable/nginx-ingress
helm template --output-dir base --namespace ingress --values values.yaml ingress-controller charts/nginx-ingress
cat <<EOF > base/nginx-ingress/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: ingress
EOF
cd base/nginx-ingress
kustomize create --autodetect
```
After running these commands against your own Helm chart, you will have a Kustomize directory structure that can be used as a Wild Cloud app. All you need to do then, usually, is:
- add an app manifest (a `manifest.yaml` file).
- replace any hardcoded operator values with Wild Cloud operator variables, such as `{{ .cloud.domain }}` for the domain name.
- modify how secrets are referenced in the Kustomize files (see above)
- update labels and selectors to use the Wild Cloud standard:
- Replace complex Helm labels (like `app.kubernetes.io/name`, `app.kubernetes.io/instance`) with simple component labels
- Use `component: web`, `component: worker`, etc. in selectors and pod template labels
- Let Kustomize handle the common labels (`app`, `managedBy`, `partOf`) automatically
- remove any Helm-specific labels from the Kustomize files, as Wild Cloud apps do not use Helm labels.
Contributions are welcome via pull requests. Ensure your app follows the Wild Cloud conventions and includes all required files.
## Notice: Third-Party Software

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: 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:
- apps.discourse.adminPassword
- apps.discourse.dbPassword
- apps.discourse.dbUrl
- apps.redis.password
- apps.discourse.secretKeyBase
- apps.discourse.smtpPassword
- apps.postgres.password
- 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: adminPassword
- key: dbPassword
- key: smtpPassword
requiredSecrets:
- apps.ghost.adminPassword
- apps.ghost.dbPassword
- apps.ghost.smtpPassword
- 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

@@ -20,7 +20,7 @@ Sensitive configuration is stored in the `gitea-secrets` secret and managed by t
- `dbPassword` - Database password
- `smtpPassword` - SMTP authentication password
Secrets are defined in `secrets.yaml` and listed in `manifest.yaml` under `requiredSecrets`. The `wild-app-deploy` command automatically ensures all required secrets exist in the `gitea-secrets` secret before deployment.
Secrets are defined in `secrets.yaml` and listed in `manifest.yaml` under `defaultSecrets`. When deploying, the system automatically ensures all required secrets exist in the `gitea-secrets` secret before deployment.
### Persistent Configuration (app.ini)
Gitea manages its own `app.ini` file on persistent storage for:
@@ -41,13 +41,13 @@ Gitea manages its own `app.ini` file on persistent storage for:
### Non-Secret Settings
1. Edit `gitea.env` with your changes
2. Run `wild-app-deploy gitea` to apply changes
2. Deploy the app via the web app, CLI, or API to apply changes
3. Pod will restart and pick up new configuration
### Secret Settings
1. Edit `secrets.yaml` with your secret values
2. Ensure the secret key is listed in `manifest.yaml` under `requiredSecrets`
3. Run `wild-app-deploy gitea` - this will automatically update the `gitea-secrets` secret and restart the pod
2. Ensure the secret key is listed in `manifest.yaml` under `defaultSecrets`
3. Deploy the app via the web app, CLI, or API - this will automatically update the `gitea-secrets` secret and restart the pod
### Web UI Changes
Configuration changes made through Gitea's admin web interface are automatically persisted to the `app.ini` file on persistent storage and will survive pod restarts.

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: TBD
port: 465
from: no-reply@{{ .cloud.domain }}
user: TBD
host: '{{ .cloud.smtp.host }}'
port: '{{ .cloud.smtp.port }}'
user: '{{ .cloud.smtp.user }}'
from: '{{ .cloud.smtp.from }}'
defaultSecrets:
- key: adminPassword
- key: dbPassword
- key: secretKey
- key: jwtSecret
- key: smtpPassword
requiredSecrets:
- apps.gitea.adminPassword
- apps.gitea.dbPassword
- apps.gitea.secretKey
- apps.gitea.jwtSecret
- apps.gitea.smtpPassword
- 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
@@ -19,7 +23,8 @@ defaultConfig:
dbUsername: immich
domain: immich.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
defaultSecrets:
- key: dbPassword
requiredSecrets:
- apps.immich.dbPassword
- apps.postgres.password
- apps.redis.password
- 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: 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:
- apps.keila.secretKeyBase
- apps.keila.dbPassword
- apps.keila.dbUrl
- apps.keila.adminPassword
- apps.keila.smtpPassword
- apps.postgres.password
- 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
@@ -14,7 +19,9 @@ defaultConfig:
dbUser: listmonk
dbSSLMode: disable
timezone: UTC
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: 'postgres://{{ .app.dbUser }}:{{ .secrets.dbPassword }}@{{ .app.dbHost }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode={{ .app.dbSSLMode }}'
requiredSecrets:
- apps.listmonk.dbPassword
- apps.listmonk.dbUrl
- apps.postgres.password
- 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 }}

55
loomio/db-init-job.yaml Normal file
View File

@@ -0,0 +1,55 @@
apiVersion: batch/v1
kind: Job
metadata:
name: loomio-db-init
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: db-init
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: RAILS_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: loomio-secrets
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: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
securityContext:
runAsNonRoot: false
runAsUser: 0
seccompProfile:
type: RuntimeDefault

View File

@@ -0,0 +1,106 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: loomio-worker
spec:
replicas: 1
selector:
matchLabels:
component: worker
template:
metadata:
labels:
component: worker
spec:
containers:
- name: worker
image: {{ .workerImage }}
env:
- name: TASK
value: worker
- name: RAILS_ENV
value: production
- name: SITE_NAME
value: {{ .appName }}
- name: CANONICAL_HOST
value: {{ .domain }}
- name: PUBLIC_APP_URL
value: https://{{ .domain }}
- name: SUPPORT_EMAIL
value: {{ .supportEmail }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: loomio-secrets
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
- name: ACTIVE_STORAGE_SERVICE
value: {{ .activeStorageService }}
- name: SMTP_AUTH
value: {{ .smtp.auth }}
- name: SMTP_DOMAIN
value: {{ .smtp.domain }}
- name: SMTP_SERVER
value: {{ .smtp.host }}
- name: SMTP_PORT
value: "{{ .smtp.port }}"
- name: SMTP_USERNAME
value: {{ .smtp.user }}
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: loomio-secrets
key: smtpPassword
- name: SMTP_USE_SSL
value: "{{ .smtp.tls }}"
- name: REPLY_HOSTNAME
value: {{ .smtp.from }}
- name: BUNDLE_APP_CONFIG
value: /loomio/tmp/.bundle
volumeMounts:
- name: uploads
mountPath: /loomio/public/system
- name: storage
mountPath: /loomio/storage
- name: tmp
mountPath: /loomio/tmp
- name: log
mountPath: /loomio/log
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
volumes:
- name: uploads
persistentVolumeClaim:
claimName: loomio-uploads
- name: storage
persistentVolumeClaim:
claimName: loomio-storage
- name: tmp
emptyDir: {}
- name: log
emptyDir: {}

134
loomio/deployment.yaml Normal file
View File

@@ -0,0 +1,134 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: loomio
spec:
replicas: 1
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
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
env:
- name: RAILS_ENV
value: production
- name: SITE_NAME
value: {{ .appName }}
- name: CANONICAL_HOST
value: {{ .domain }}
- name: PUBLIC_APP_URL
value: https://{{ .domain }}
- name: SUPPORT_EMAIL
value: {{ .supportEmail }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: loomio-secrets
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
- name: FORCE_SSL
value: "{{ .forceSSL }}"
- name: USE_RACK_ATTACK
value: "{{ .useRackAttack }}"
- name: PUMA_WORKERS
value: "{{ .pumaWorkers }}"
- name: MIN_THREADS
value: "{{ .minThreads }}"
- name: MAX_THREADS
value: "{{ .maxThreads }}"
- name: ACTIVE_STORAGE_SERVICE
value: {{ .activeStorageService }}
- name: SMTP_AUTH
value: {{ .smtp.auth }}
- name: SMTP_DOMAIN
value: {{ .smtp.domain }}
- name: SMTP_SERVER
value: {{ .smtp.host }}
- name: SMTP_PORT
value: "{{ .smtp.port }}"
- name: SMTP_USERNAME
value: {{ .smtp.user }}
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: loomio-secrets
key: smtpPassword
- 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
- name: storage
mountPath: /loomio/storage
- name: tmp
mountPath: /loomio/tmp
- name: log
mountPath: /loomio/log
resources:
requests:
memory: 512Mi
cpu: 200m
limits:
memory: 2Gi
cpu: 1000m
livenessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
securityContext:
runAsNonRoot: false
runAsUser: 0
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
volumes:
- name: uploads
persistentVolumeClaim:
claimName: loomio-uploads
- name: storage
persistentVolumeClaim:
claimName: loomio-storage
- name: tmp
emptyDir: {}
- name: log
emptyDir: {}

24
loomio/ingress.yaml Normal file
View File

@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: loomio
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: loomio
port:
number: 80

20
loomio/kustomization.yaml Normal file
View File

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

57
loomio/manifest.yaml Normal file
View File

@@ -0,0 +1,57 @@
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/brand/logo_gold.svg
requires:
- name: postgres
installed_as: postgres
- name: redis
defaultConfig:
namespace: loomio
externalDnsDomain: "{{ .cloud.domain }}"
image: loomio/loomio:latest
workerImage: loomio/loomio:latest
appName: Loomio
domain: "loomio.{{ .cloud.domain }}"
tlsSecretName: wildcard-wild-cloud-tls
port: 3000
storage:
uploads: 5Gi
files: 5Gi
plugins: 1Gi
redisUrl: "{{ .apps.redis.uri }}"
adminEmail: "{{ .operator.email }}"
supportEmail: "{{ .operator.email }}"
forceSSL: "1"
useRackAttack: "1"
pumaWorkers: "2"
minThreads: "5"
maxThreads: "5"
activeStorageService: local
db:
name: loomio
user: loomio
host: "{{ .apps.postgres.host }}"
port: "{{ .apps.postgres.port }}"
smtp:
auth: plain
domain: "{{ .cloud.domain }}"
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
tls: "{{ .cloud.smtp.tls }}"
from: "{{ .cloud.smtp.from }}"
defaultSecrets:
- key: dbPassword
default: "{{ random.AlphaNum 32 }}"
- key: dbUrl
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30"
- key: deviseSecret
default: "{{ random.AlphaNum 32 }}"
- key: secretCookieToken
default: "{{ random.AlphaNum 32 }}"
- key: smtpPassword
default: "{{ .secrets.smtp.password }}"
requiredSecrets:
- postgres.password

4
loomio/namespace.yaml Normal file
View File

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

11
loomio/pvc-storage.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: loomio-storage
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .storage.files }}
storageClassName: longhorn

11
loomio/pvc-uploads.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: loomio-uploads
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .storage.uploads }}
storageClassName: longhorn

13
loomio/service.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: loomio
spec:
type: ClusterIP
selector:
component: web
ports:
- name: http
port: 80
targetPort: 3000
protocol: TCP

30
mailu/configmap.yaml Normal file
View File

@@ -0,0 +1,30 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mailu-config
namespace: {{ .namespace }}
data:
DOMAIN: "{{ .domain }}"
HOSTNAMES: "{{ .hostname }}"
POSTMASTER: "admin"
TZ: "{{ .timezone }}"
TLS_FLAVOR: "cert"
MESSAGE_SIZE_LIMIT: "50000000"
MESSAGE_RATELIMIT: "200/day"
RELAYNETS: ""
RELAYHOST: "{{ .relayHost }}"
RELAYPORT: "{{ .relayPort }}"
FETCHMAIL_ENABLED: "false"
RECIPIENT_DELIMITER: "+"
DMARC_RUA: "admin"
DMARC_RUF: "admin"
WELCOME: "false"
WELCOME_SUBJECT: "Welcome to your new email account"
WELCOME_BODY: "Welcome! You can now use your email account."
ADMIN: "true"
WEB_ADMIN: "/admin"
WEB_WEBMAIL: "/webmail"
WEBMAIL: "roundcube"
SITENAME: "Mailu"
WEBSITE: "https://{{ .hostname }}"
LOG_LEVEL: "{{ .logLevel }}"

103
mailu/deployment-admin.yaml Normal file
View File

@@ -0,0 +1,103 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: admin
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: admin
template:
metadata:
labels:
component: admin
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- {{ .unbound.ip }}
searches:
- {{ .namespace }}.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "5"
initContainers:
- name: fix-permissions
image: busybox:latest
command: ['sh', '-c', 'chown -R 999:999 /data /dkim']
volumeMounts:
- name: data
subPath: admin
mountPath: /data
- name: data
subPath: dkim
mountPath: /dkim
containers:
- name: admin
image: {{ .images.admin }}
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add:
- SYS_CHROOT
- CHOWN
- SETGID
- SETUID
env:
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: mailu-secrets
key: secretKey
- name: REDIS_ADDRESS
value: "{{ .redis.host }}"
- name: I_KNOW_MY_SETUP_DOESNT_FIT_REQUIREMENTS_AND_WONT_FILE_ISSUES_WITHOUT_PATCHES
value: "true"
- name: INITIAL_ADMIN_ACCOUNT
value: "{{ .initialAccount.username }}"
- name: INITIAL_ADMIN_DOMAIN
value: "{{ .initialAccount.domain }}"
- name: INITIAL_ADMIN_PW
valueFrom:
secretKeyRef:
name: mailu-secrets
key: initialAccountPassword
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data
subPath: admin
mountPath: /data
- name: data
subPath: dkim
mountPath: /dkim
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /ping
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /ping
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: mailu-storage

View File

@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: dovecot
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: dovecot
template:
metadata:
labels:
component: dovecot
spec:
initContainers:
- name: fix-permissions
image: busybox:latest
command: ['sh', '-c', 'chown -R 999:999 /data /mail']
volumeMounts:
- name: data
subPath: mail
mountPath: /mail
- name: data
subPath: dovecot
mountPath: /data
containers:
- name: dovecot
image: {{ .images.dovecot }}
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add:
- SYS_CHROOT
- CHOWN
- SETGID
- SETUID
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: imap
containerPort: 143
- name: imaps
containerPort: 993
- name: pop3
containerPort: 110
- name: pop3s
containerPort: 995
- name: sieve
containerPort: 4190
- name: auth
containerPort: 2102
- name: lmtp
containerPort: 2525
volumeMounts:
- name: data
subPath: mail
mountPath: /mail
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
volumes:
- name: data
persistentVolumeClaim:
claimName: mailu-storage

View File

@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: front
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: front
template:
metadata:
labels:
component: front
spec:
containers:
- name: front
image: {{ .images.front }}
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add:
- SYS_CHROOT
- CHOWN
- SETGID
- SETUID
- NET_BIND_SERVICE
env:
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: mailu-secrets
key: secretKey
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
- name: smtp
containerPort: 25
- name: smtps
containerPort: 465
- name: submission
containerPort: 587
- name: imap
containerPort: 143
- name: imaps
containerPort: 993
- name: pop3
containerPort: 110
- name: pop3s
containerPort: 995
volumeMounts:
- name: certs
mountPath: /certs
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: certs
secret:
secretName: {{ .tlsSecretName }}
optional: true

View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: postfix
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: postfix
template:
metadata:
labels:
component: postfix
spec:
initContainers:
- name: fix-permissions
image: busybox:latest
command: ['sh', '-c', 'chown -R 999:999 /queue']
volumeMounts:
- name: data
subPath: mailqueue
mountPath: /queue
containers:
- name: postfix
image: {{ .images.postfix }}
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add:
- SYS_CHROOT
- CHOWN
- SETGID
- SETUID
- NET_BIND_SERVICE
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: smtp
containerPort: 25
- name: smtps
containerPort: 465
- name: submission
containerPort: 587
volumeMounts:
- name: data
subPath: mailqueue
mountPath: /queue
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
volumes:
- name: data
persistentVolumeClaim:
claimName: mailu-storage

View File

@@ -0,0 +1,56 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: redis
template:
metadata:
labels:
component: redis
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: redis
image: {{ .images.redis }}
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
ports:
- name: redis
containerPort: 6379
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}

View File

@@ -0,0 +1,45 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: rspamd
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: rspamd
template:
metadata:
labels:
component: rspamd
spec:
containers:
- name: rspamd
image: {{ .images.rspamd }}
imagePullPolicy: IfNotPresent
env:
- name: REDIS_ADDRESS
value: "{{ .redis.host }}:{{ .redis.port }}"
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: rspamd
containerPort: 11332
- name: http
containerPort: 11334
volumeMounts:
- name: data
subPath: rspamd
mountPath: /var/lib/rspamd
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "2000m"
volumes:
- name: data
persistentVolumeClaim:
claimName: mailu-storage

View File

@@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: unbound
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: unbound
template:
metadata:
labels:
component: unbound
spec:
containers:
- name: unbound
image: {{ .unbound.image }}
imagePullPolicy: IfNotPresent
envFrom:
- configMapRef:
name: mailu-config
env:
- name: UNBOUND_TLS_NAME
value: "dns"
ports:
- name: dns
containerPort: 53
protocol: UDP
- name: dns-tcp
containerPort: 53
protocol: TCP
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
tcpSocket:
port: 53
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 53
initialDelaySeconds: 5
periodSeconds: 5

View File

@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: webmail
namespace: {{ .namespace }}
spec:
replicas: 1
selector:
matchLabels:
component: webmail
template:
metadata:
labels:
component: webmail
spec:
initContainers:
- name: fix-permissions
image: busybox:latest
command: ['sh', '-c', 'chown -R 999:999 /data']
volumeMounts:
- name: data
subPath: webmail
mountPath: /data
containers:
- name: webmail
image: {{ .images.webmail }}
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add:
- SYS_CHROOT
- CHOWN
- SETGID
- SETUID
env:
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: mailu-secrets
key: secretKey
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: http
containerPort: 80
volumeMounts:
- name: data
subPath: webmail
mountPath: /data
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
volumes:
- name: data
persistentVolumeClaim:
claimName: mailu-storage

42
mailu/ingress.yaml Normal file
View File

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

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