Compare commits

..

8 Commits

25 changed files with 2258 additions and 2291 deletions

View File

@ -2,7 +2,7 @@ apiVersion: v2
name: donetick name: donetick
description: Donetick helm chart for Kubernetes description: Donetick helm chart for Kubernetes
type: application type: application
version: 1.0.4 version: 1.0.6
appVersion: "v0.1.60" appVersion: "v0.1.60"
maintainers: maintainers:
- name: Richard Tomik - name: Richard Tomik

View File

@ -1,14 +1,12 @@
# Donetick Helm Chart # Donetick Helm Chart
A Helm chart for deploying the Donetick task management application on Kubernetes. A Helm chart for deploying [Donetick](https://github.com/donetick/donetick) on Kubernetes.
## Introduction ## Introduction
This chart deploys [Donetick](https://github.com/donetick/donetick) on a Kubernetes cluster using the Helm package manager. This chart deploys Donetick, a task management application, on a Kubernetes cluster using the Helm package manager. Donetick supports SQLite or PostgreSQL databases, real-time updates via WebSockets, OAuth2 authentication, and push notifications via Telegram and Pushover.
Source code can be found here:
- https://github.com/rtomik/helm-charts/tree/main/charts/donetick
Source code: https://github.com/rtomik/helm-charts/tree/main/charts/donetick
## Prerequisites ## Prerequisites
@ -18,98 +16,72 @@ Source code can be found here:
## Installing the Chart ## Installing the Chart
To install the chart with the release name `donetick`:
```bash ```bash
$ helm repo add donetick-chart https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
$ helm install donetick donetick-chart/donetick helm install donetick rtomik/donetick
``` ```
> **Tip**: List all releases using `helm list` ## Uninstalling the Chart
```bash
helm uninstall donetick
```
## Configuration Examples ## Configuration Examples
### Basic Installation with SQLite (Default) ### Minimal Installation (SQLite)
The chart works out of the box with SQLite — no additional configuration required:
```bash ```bash
helm install donetick donetick-chart/donetick helm install donetick rtomik/donetick
``` ```
### Installation with External PostgreSQL ### PostgreSQL Configuration
Create a values file for PostgreSQL configuration: ```yaml
config:
database:
type: "postgres"
host: "postgresql.database.svc.cluster.local"
port: 5432
name: "donetick"
secrets:
existingSecret: "donetick-postgres-secret"
userKey: "username"
passwordKey: "password"
jwt:
secret: "your-secure-jwt-secret-at-least-32-characters-long"
server:
cors_allow_origins:
- "https://your-domain.com"
features:
notifications: true
realtime: true
ingress:
enabled: true
hosts:
- host: donetick.your-domain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- donetick.your-domain.com
persistence:
enabled: true
size: "5Gi"
```
### Production with Existing Secrets
```yaml ```yaml
# values-postgres.yaml
config: config:
database:
type: "postgres"
host: "postgresql.database.svc.cluster.local"
port: 5432
user: "donetick"
password: "your-secure-password"
name: "donetick"
migration: true
# Update JWT secret for production
jwt:
secret: "your-secure-jwt-secret-at-least-32-characters-long"
# Configure server settings
server:
cors_allow_origins:
- "https://your-domain.com"
- "http://localhost:5173"
# Enable features as needed
features:
notifications: true
realtime: true
oauth: false
# Enable ingress for external access
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: donetick.your-domain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: donetick-tls
hosts:
- donetick.your-domain.com
# Configure persistence
persistence:
enabled: true
storageClass: "fast-ssd"
size: "5Gi"
```
Install with PostgreSQL configuration:
```bash
helm install donetick donetick-chart/donetick -f values-postgres.yaml
```
### Production Installation with External Secrets
For production deployments, use Kubernetes secrets for sensitive data:
```yaml
# values-production.yaml
config:
database:
type: "postgres"
host: "postgresql.database.svc.cluster.local"
port: 5432
name: "donetick"
# Use existing secret for postgres credentials
database: database:
type: "postgres" type: "postgres"
host: "postgresql.database.svc.cluster.local" host: "postgresql.database.svc.cluster.local"
@ -120,14 +92,11 @@ config:
userKey: "username" userKey: "username"
passwordKey: "password" passwordKey: "password"
# Use existing secret for JWT
jwt: jwt:
existingSecret: "donetick-jwt-secret" existingSecret: "donetick-jwt-secret"
secretKey: "jwt-secret" secretKey: "jwtSecret"
session_time: "168h" session_time: "168h"
max_refresh: "168h"
# OAuth2 configuration with secrets
oauth2: oauth2:
existingSecret: "donetick-oauth-secret" existingSecret: "donetick-oauth-secret"
clientIdKey: "client-id" clientIdKey: "client-id"
@ -137,26 +106,17 @@ config:
user_info_url: "https://your-oauth-provider.com/userinfo" user_info_url: "https://your-oauth-provider.com/userinfo"
redirect_url: "https://donetick.your-domain.com/auth/callback" redirect_url: "https://donetick.your-domain.com/auth/callback"
# Production server settings
server: server:
cors_allow_origins: cors_allow_origins:
- "https://donetick.your-domain.com" - "https://donetick.your-domain.com"
rate_limit: 100 rate_limit: 100
rate_period: "60s" rate_period: "60s"
# Enable production features
features: features:
notifications: true notifications: true
realtime: true realtime: true
oauth: true oauth: true
# Security context for production
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# Resource limits for production
resources: resources:
limits: limits:
cpu: 500m cpu: 500m
@ -165,299 +125,244 @@ resources:
cpu: 100m cpu: 100m
memory: 128Mi memory: 128Mi
# Ingress with TLS
ingress: ingress:
enabled: true enabled: true
className: "nginx"
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts: hosts:
- host: donetick.your-domain.com - host: donetick.your-domain.com
paths: paths:
- path: / - path: /
pathType: Prefix pathType: Prefix
tls: tls:
- secretName: donetick-tls - hosts:
hosts:
- donetick.your-domain.com - donetick.your-domain.com
``` ```
Create the required secrets: Create the required secrets:
```bash ```bash
# Postgres secret
kubectl create secret generic donetick-postgres-secret \ kubectl create secret generic donetick-postgres-secret \
--from-literal=username='donetick' \ --from-literal=username='donetick' \
--from-literal=password='your-secure-db-password' --from-literal=password='your-secure-db-password'
# JWT secret
kubectl create secret generic donetick-jwt-secret \ kubectl create secret generic donetick-jwt-secret \
--from-literal=jwt-secret='your-very-secure-jwt-secret-at-least-32-characters-long' --from-literal=jwtSecret='your-very-secure-jwt-secret-at-least-32-characters-long'
# OAuth secret (if using OAuth)
kubectl create secret generic donetick-oauth-secret \ kubectl create secret generic donetick-oauth-secret \
--from-literal=client-id='your-oauth-client-id' \ --from-literal=client-id='your-oauth-client-id' \
--from-literal=client-secret='your-oauth-client-secret' --from-literal=client-secret='your-oauth-client-secret'
``` ```
Install with production configuration:
```bash
helm install donetick donetick-chart/donetick -f values-production.yaml
```
## Uninstalling the Chart
To uninstall/delete the `donetick` deployment:
```bash
helm uninstall donetick
```
## Parameters ## Parameters
### Global parameters ### Global Parameters
| Name | Description | Value | | Name | Description | Default |
|------------------------|-------------------------------------------------------------------------------------|-------| |------|-------------|---------|
| `nameOverride` | String to partially override the release name | `""` | | `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | String to fully override the release name | `""` | | `fullnameOverride` | Fully override the release name | `""` |
### Image parameters ### Image Parameters
| Name | Description | Value | | Name | Description | Default |
|-------------------------|--------------------------------------------------------------------------------------|--------------------| |------|-------------|---------|
| `image.repository` | Donetick image repository | `donetick/donetick` | | `image.repository` | Donetick image repository | `donetick/donetick` |
| `image.tag` | Donetick image tag | `v0.1.60` | | `image.tag` | Image tag | `v0.1.60` |
| `image.pullPolicy` | Donetick image pull policy | `IfNotPresent` | | `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Global Docker registry secret names as an array | `[]` | | `imagePullSecrets` | Image pull secrets | `[]` |
### Secret Management ### Deployment Parameters
| Name | Description | Value | | Name | Description | Default |
|----------------------------------------|--------------------------------------------------------------------|---------------------| |------|-------------|---------|
| `config.jwt.existingSecret` | Name of existing secret for JWT token | `""` | | `replicaCount` | Number of replicas | `1` |
| `config.jwt.secretKey` | Key in the existing secret for JWT token | `"jwtSecret"` | | `revisionHistoryLimit` | Revisions to retain | `3` |
| `config.oauth2.existingSecret` | Name of existing secret for OAuth2 credentials | `""` | | `startupArgs` | Optional startup arguments | `[]` |
| `config.oauth2.clientIdKey` | Key in the existing secret for OAuth2 client ID | `"client-id"` | | `podSecurityContext.runAsNonRoot` | Run as non-root | `true` |
| `config.oauth2.clientSecretKey` | Key in the existing secret for OAuth2 client secret | `"client-secret"` | | `podSecurityContext.runAsUser` | User ID | `1000` |
| `config.database.secrets.existingSecret` | Name of existing secret for postgres credentials | `""` | | `podSecurityContext.fsGroup` | Filesystem group ID | `1000` |
| `config.database.secrets.userKey` | Key in the existing secret for postgres username | `"username"` | | `nodeSelector` | Node selector | `{}` |
| `config.database.secrets.passwordKey` | Key in the existing secret for postgres password | `"password"` | | `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
| `podAnnotations` | Pod annotations | `{}` |
### Deployment parameters ### Service Parameters
| Name | Description | Value | | Name | Description | Default |
|--------------------------------------|--------------------------------------------------------------------------|-----------| |------|-------------|---------|
| `replicaCount` | Number of Donetick replicas | `1` | | `service.type` | Service type | `ClusterIP` |
| `revisionHistoryLimit` | Number of revisions to retain for rollback | `3` | | `service.port` | Service port | `2021` |
| `podSecurityContext.runAsNonRoot` | Run containers as non-root user | `true` | | `service.annotations` | Service annotations | `{}` |
| `podSecurityContext.runAsUser` | User ID for the container | `1000` |
| `podSecurityContext.fsGroup` | Group ID for the container filesystem | `1000` |
| `containerSecurityContext` | Security context for the container | See values.yaml |
| `nodeSelector` | Node labels for pod assignment | `{}` |
| `tolerations` | Tolerations for pod assignment | `[]` |
| `affinity` | Affinity for pod assignment | `{}` |
### Service parameters ### Ingress Parameters
| Name | Description | Value | | Name | Description | Default |
|----------------------------|------------------------------------------------------|-------------| |------|-------------|---------|
| `service.type` | Kubernetes Service type | `ClusterIP` | | `ingress.enabled` | Enable ingress | `false` |
| `service.port` | Service HTTP port | `2021` | | `ingress.className` | Ingress class name | `""` |
| `service.annotations` | Additional annotations for Service | `{}` | | `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
### Pod Configuration
| Name | Description | Value |
|----------------------------|------------------------------------------------------|-------------|
| `podAnnotations` | Additional annotations for pods | `{}` |
### Ingress parameters
| Name | Description | Value |
|----------------------------|------------------------------------------------------|----------------------|
| `ingress.enabled` | Enable ingress record generation | `true` |
| `ingress.className` | IngressClass name | `"traefik"` |
| `ingress.annotations` | Additional annotations for the Ingress resource | See values.yaml |
| `ingress.hosts` | Array of host and path objects | See values.yaml |
| `ingress.tlsSecretName` | Global TLS secret name for all hosts | `""` |
| `ingress.tls` | TLS configuration | See values.yaml | | `ingress.tls` | TLS configuration | See values.yaml |
| `ingress.tls[].secretName` | Host-specific TLS secret name (overrides global) | `""` |
### Persistence parameters ### Persistence Parameters
| Name | Description | Value | | Name | Description | Default |
|-------------------------------|------------------------------------------------------|---------------| |------|-------------|---------|
| `persistence.enabled` | Enable persistence using PVC | `false` | | `persistence.enabled` | Enable persistence | `false` |
| `persistence.storageClass` | PVC Storage Class | `""` | | `persistence.storageClass` | Storage class | `""` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` | | `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | PVC Size | `1Gi` | | `persistence.size` | PVC size | `1Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Health Checks
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `probes.startup.enabled` | Enable startup probe | `true` |
| `probes.startup.initialDelaySeconds` | Initial delay for startup probe | `10` |
| `probes.startup.periodSeconds` | Period for startup probe | `10` |
| `probes.startup.failureThreshold` | Failure threshold for startup probe | `30` |
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.initialDelaySeconds` | Initial delay for liveness probe | `30` |
| `probes.liveness.periodSeconds` | Period for liveness probe | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.initialDelaySeconds` | Initial delay for readiness probe | `5` |
| `probes.readiness.periodSeconds` | Period for readiness probe | `5` |
### Application Configuration
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.name` | Application name | `selfhosted` |
| `config.is_done_tick_dot_com` | Enable donetick.com features | `false` |
| `config.is_user_creation_disabled` | Disable user registration | `false` |
### Real-time Configuration
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.realtime.max_connections` | Maximum WebSocket connections | `100` |
| `config.realtime.ping_interval` | WebSocket ping interval | `30s` |
| `config.realtime.pong_wait` | WebSocket pong wait timeout | `60s` |
| `config.realtime.write_wait` | WebSocket write timeout | `10s` |
| `config.realtime.max_message_size` | Maximum WebSocket message size | `512` |
### Database Configuration ### Database Configuration
| Name | Description | Value | | Name | Description | Default |
|----------------------------------------|------------------------------------------------------|---------------| |------|-------------|---------|
| `config.database.type` | Database type (sqlite or postgres) | `sqlite` | | `config.database.type` | Database type (`sqlite` or `postgres`) | `sqlite` |
| `config.database.migration` | Enable database migrations | `true` | | `config.database.migration` | Enable migrations | `true` |
| `config.database.host` | PostgreSQL host (when type=postgres) | `""` | | `config.database.migration_skip` | Skip migrations | `false` |
| `config.database.port` | PostgreSQL port (when type=postgres) | `5432` | | `config.database.migration_retry` | Migration retry count | `3` |
| `config.database.user` | PostgreSQL user (when type=postgres) | `""` | | `config.database.migration_timeout` | Migration timeout | `600s` |
| `config.database.password` | PostgreSQL password (when type=postgres) | `""` | | `config.database.host` | PostgreSQL host | `""` |
| `config.database.name` | PostgreSQL database name (when type=postgres) | `""` | | `config.database.port` | PostgreSQL port | `5432` |
| `config.database.name` | PostgreSQL database name | `""` |
| `config.database.secrets.existingSecret` | Existing secret for credentials | `""` |
| `config.database.secrets.userKey` | Key for username in secret | `username` |
| `config.database.secrets.passwordKey` | Key for password in secret | `password` |
### JWT Configuration ### JWT Configuration
| Name | Description | Value | | Name | Description | Default |
|----------------------------------------|------------------------------------------------------|---------------| |------|-------------|---------|
| `config.jwt.secret` | JWT signing secret | `changeme-this-secret-should-be-at-least-32-characters-long` | | `config.jwt.secret` | JWT signing secret (min 32 chars) | `changeme-...` |
| `config.jwt.session_time` | JWT session duration | `168h` | | `config.jwt.session_time` | Session duration | `168h` |
| `config.jwt.max_refresh` | JWT maximum refresh duration | `168h` | | `config.jwt.max_refresh` | Max refresh duration | `168h` |
| `config.jwt.existingSecret` | Existing secret for JWT | `""` |
| `config.jwt.secretKey` | Key in secret | `jwtSecret` |
### Server Configuration ### Server Configuration
| Name | Description | Value | | Name | Description | Default |
|----------------------------------------|------------------------------------------------------|---------------| |------|-------------|---------|
| `config.server.port` | Server port | `2021` | | `config.server.port` | Server port | `2021` |
| `config.server.read_timeout` | Server read timeout | `10s` | | `config.server.read_timeout` | Read timeout | `10s` |
| `config.server.write_timeout` | Server write timeout | `10s` | | `config.server.write_timeout` | Write timeout | `10s` |
| `config.server.rate_period` | Rate limiting period | `60s` | | `config.server.rate_period` | Rate limiting period | `60s` |
| `config.server.rate_limit` | Rate limiting requests per period | `300` | | `config.server.rate_limit` | Rate limit per period | `300` |
| `config.server.serve_frontend` | Serve frontend files | `true` | | `config.server.serve_frontend` | Serve frontend files | `true` |
| `config.server.cors_allow_origins` | CORS allowed origins | See values.yaml |
### OAuth2 Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.oauth2.client_id` | OAuth2 client ID | `""` |
| `config.oauth2.client_secret` | OAuth2 client secret | `""` |
| `config.oauth2.existingSecret` | Existing secret for credentials | `""` |
| `config.oauth2.clientIdKey` | Key for client ID in secret | `client-id` |
| `config.oauth2.clientSecretKey` | Key for client secret in secret | `client-secret` |
| `config.oauth2.auth_url` | Authorization URL | `""` |
| `config.oauth2.token_url` | Token URL | `""` |
| `config.oauth2.user_info_url` | User info URL | `""` |
| `config.oauth2.redirect_url` | Redirect URL | `""` |
### Real-time Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.realtime.max_connections` | Max WebSocket connections | `100` |
| `config.realtime.ping_interval` | Ping interval | `30s` |
| `config.realtime.pong_wait` | Pong wait timeout | `60s` |
| `config.realtime.write_wait` | Write timeout | `10s` |
| `config.realtime.max_message_size` | Max message size | `512` |
### Notification Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.telegram.token` | Telegram bot token | `""` |
| `config.pushover.token` | Pushover token | `""` |
### Feature Flags ### Feature Flags
| Name | Description | Value | | Name | Description | Default |
|----------------------------------------|------------------------------------------------------|---------------| |------|-------------|---------|
| `config.features.notifications` | Enable notifications | `true` | | `config.features.notifications` | Enable notifications | `true` |
| `config.features.realtime` | Enable real-time features | `true` | | `config.features.realtime` | Enable real-time features | `true` |
| `config.features.oauth` | Enable OAuth authentication | `false` | | `config.features.oauth` | Enable OAuth | `false` |
| `config.is_user_creation_disabled` | Disable user registration | `false` |
## Database Setup ### Resource Parameters
### PostgreSQL Requirements | Name | Description | Default |
|------|-------------|---------|
| `resources` | Resource limits and requests | `{}` |
When using PostgreSQL, ensure you have: ### Health Check Parameters
1. **Database Created**: Create a database for Donetick | Name | Description | Default |
```sql |------|-------------|---------|
CREATE DATABASE donetick; | `probes.startup.enabled` | Enable startup probe | `true` |
CREATE USER donetick WITH PASSWORD 'your-secure-password'; | `probes.startup.path` | Startup probe path | `/health` |
GRANT ALL PRIVILEGES ON DATABASE donetick TO donetick; | `probes.startup.initialDelaySeconds` | Startup initial delay | `30` |
``` | `probes.startup.periodSeconds` | Startup period | `15` |
| `probes.startup.failureThreshold` | Startup failure threshold | `80` |
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Liveness probe path | `/health` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `30` |
| `probes.liveness.periodSeconds` | Liveness period | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/health` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `5` |
| `probes.readiness.periodSeconds` | Readiness period | `5` |
2. **Network Access**: Ensure Donetick can reach your PostgreSQL instance ### Autoscaling Parameters
3. **Proper Credentials**: Configure database credentials in values or secrets
### Database Migration | Name | Description | Default |
|------|-------------|---------|
Donetick automatically runs database migrations on startup when `config.database.migration: true`. For production: | `autoscaling.enabled` | Enable HPA | `false` |
| `autoscaling.minReplicas` | Min replicas | `1` |
1. **Review Migrations**: Check what migrations will be applied | `autoscaling.maxReplicas` | Max replicas | `5` |
2. **Backup Database**: Always backup before running migrations | `autoscaling.targetCPUUtilizationPercentage` | Target CPU | `80` |
3. **Monitor Startup**: Watch pod logs during initial deployment | `autoscaling.targetMemoryUtilizationPercentage` | Target memory | `80` |
## Troubleshooting ## Troubleshooting
### Common Issues ### Real-time Configuration Panic
#### 1. Real-time Configuration Panic
**Error**: `Invalid real-time configuration: maxConnections must be positive, got 0` **Error**: `Invalid real-time configuration: maxConnections must be positive, got 0`
**Solution**: Ensure the real-time configuration is properly set: Ensure `config.realtime.max_connections` is set to a positive value (default: `100`).
```yaml
config:
realtime:
max_connections: 100 # Must be > 0
```
#### 2. Database Connection Issues ### Database Connection Issues
**Error**: Database connection failures
**Solutions**:
- Verify PostgreSQL is running and accessible - Verify PostgreSQL is running and accessible
- Check database credentials in secrets - Check database credentials in secrets
- Ensure database name exists - Ensure the database exists
- Verify network policies allow connection - Verify network policies allow the connection
#### 3. JWT Secret Issues ### JWT Authentication Failures
**Error**: JWT authentication failures
**Solution**: Ensure JWT secret is at least 32 characters: Ensure `config.jwt.secret` is at least 32 characters long. For production, use `config.jwt.existingSecret`.
```yaml
config:
jwt:
secret: "your-very-secure-jwt-secret-at-least-32-characters-long"
```
#### 4. CORS Issues ### CORS Issues
**Error**: Cross-origin request blocked
Add your domain to `config.server.cors_allow_origins`:
**Solution**: Configure CORS origins:
```yaml ```yaml
config: config:
server: server:
cors_allow_origins: cors_allow_origins:
- "https://your-domain.com" - "https://your-domain.com"
- "http://localhost:5173"
``` ```
### Debugging ### Debugging
Check application logs:
```bash ```bash
kubectl logs deployment/donetick -f kubectl logs deployment/donetick -f
```
Check configuration:
```bash
kubectl get configmap donetick-configmap -o yaml kubectl get configmap donetick-configmap -o yaml
``` ```
Verify secrets: ## Links
```bash
kubectl get secret donetick-secrets -o yaml
```
## Contributing - [Donetick GitHub](https://github.com/donetick/donetick)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/donetick)
Please feel free to contribute by opening issues or pull requests at:
https://github.com/rtomik/helm-charts
## License
This Helm chart is licensed under the MIT License.

View File

@ -4,10 +4,9 @@ A Helm chart for deploying [Jellyseerr](https://github.com/fallenbagel/jellyseer
## Introduction ## Introduction
This chart deploys Jellyseerr on a Kubernetes cluster using the Helm package manager. Jellyseerr is a fork of Overseerr for Jellyfin support. This chart deploys Jellyseerr, a media request management application for Jellyfin, on a Kubernetes cluster using the Helm package manager. Jellyseerr is a fork of Overseerr with native Jellyfin support.
Source code can be found here: Source code: https://github.com/rtomik/helm-charts/tree/main/charts/jellyseerr
- https://github.com/rtomik/helm-charts/tree/main/charts/jellyseerr
## Prerequisites ## Prerequisites
@ -17,104 +16,35 @@ Source code can be found here:
## Installing the Chart ## Installing the Chart
To install the chart with the release name `jellyseerr`:
```bash ```bash
helm repo add rtomik-charts https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
helm install jellyseerr rtomik-charts/jellyseerr helm install jellyseerr rtomik/jellyseerr
``` ```
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart ## Uninstalling the Chart
To uninstall/delete the `jellyseerr` deployment:
```bash ```bash
helm uninstall jellyseerr helm uninstall jellyseerr
``` ```
## Parameters ## Configuration Examples
### Global parameters ### Minimal Installation
| Name | Description | Value | ```yaml
|------------------------|---------------------------------------------------------------|--------| ingress:
| `nameOverride` | String to partially override the release name | `""` | enabled: true
| `fullnameOverride` | String to fully override the release name | `""` | hosts:
- host: jellyseerr.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- jellyseerr.example.com
```
### Image parameters ### Custom Timezone and Logging
| Name | Description | Value |
|-------------------------|--------------------------------------------------------------|--------------------------------|
| `image.repository` | Jellyseerr image repository | `ghcr.io/fallenbagel/jellyseerr` |
| `image.tag` | Jellyseerr image tag | `latest` |
| `image.pullPolicy` | Jellyseerr image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Global Docker registry secret names as an array | `[]` |
### Deployment parameters
| Name | Description | Value |
|--------------------------------------|--------------------------------------------------|-----------|
| `replicaCount` | Number of Jellyseerr replicas | `1` |
| `revisionHistoryLimit` | Number of revisions to retain for rollback | `3` |
| `podSecurityContext.runAsNonRoot` | Run containers as non-root user | `true` |
| `podSecurityContext.runAsUser` | User ID for the container | `1000` |
| `podSecurityContext.fsGroup` | Group ID for the container filesystem | `1000` |
| `containerSecurityContext` | Security context for the container | See values.yaml |
| `nodeSelector` | Node labels for pod assignment | `{}` |
| `tolerations` | Tolerations for pod assignment | `[]` |
| `affinity` | Affinity for pod assignment | `{}` |
### Service parameters
| Name | Description | Value |
|----------------------------|----------------------------------------------|-------------|
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `5055` |
### Ingress parameters
| Name | Description | Value |
|----------------------------|----------------------------------------------|------------------------|
| `ingress.enabled` | Enable ingress record generation | `false` |
| `ingress.className` | IngressClass name | `""` |
| `ingress.annotations` | Additional annotations for the Ingress resource | `{}` |
| `ingress.hosts` | Array of host and path objects | See values.yaml |
| `ingress.tls` | TLS configuration | `[]` |
### Persistence parameters
| Name | Description | Value |
|-------------------------------|----------------------------------------------|-----------------|
| `persistence.enabled` | Enable persistence using PVC | `true` |
| `persistence.existingClaim` | Use an existing PVC | `""` |
| `persistence.storageClass` | PVC Storage Class | `""` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` | PVC Storage Size | `1Gi` |
| `persistence.annotations` | Additional custom annotations for the PVC | `{}` |
### Environment variables
| Name | Description | Value |
|--------------------------|----------------------------------------------|-----------------|
| `env` | Environment variables for Jellyseerr | See values.yaml |
| `extraEnv` | Additional environment variables | `[]` |
### Resources parameters
| Name | Description | Value |
|--------------------------|----------------------------------------------|-----------------|
| `resources.limits` | The resources limits for containers | See values.yaml |
| `resources.requests` | The resources requests for containers | See values.yaml |
## Configuration
The following table lists the configurable parameters of the Jellyseerr chart and their default values.
### Environment Variables
You can configure Jellyseerr by setting environment variables:
```yaml ```yaml
env: env:
@ -126,20 +56,110 @@ env:
value: "5055" value: "5055"
``` ```
### Using Persistence ### Using an Existing PVC
By default, persistence is enabled with a 1Gi volume:
```yaml
persistence:
enabled: true
size: 1Gi
```
You can also use an existing PVC:
```yaml ```yaml
persistence: persistence:
enabled: true enabled: true
existingClaim: my-jellyseerr-pvc existingClaim: my-jellyseerr-pvc
``` ```
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Jellyseerr image repository | `ghcr.io/fallenbagel/jellyseerr` |
| `image.tag` | Image tag | `2.5.2` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `podSecurityContext.runAsNonRoot` | Run as non-root | `true` |
| `podSecurityContext.runAsUser` | User ID | `1000` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `1000` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
| `podAnnotations` | Pod annotations | `{}` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `5055` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | `{}` |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | `[]` |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.enabled` | Enable persistence | `true` |
| `persistence.existingClaim` | Use an existing PVC | `""` |
| `persistence.storageClass` | Storage class | `""` |
| `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | PVC size | `1Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Environment Variables
| Name | Description | Default |
|------|-------------|---------|
| `env` | Environment variables | See values.yaml |
| `extraEnv` | Additional environment variables | `[]` |
### Resource Parameters
| Name | Description | Default |
|------|-------------|---------|
| `resources` | Resource limits and requests | `{}` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Liveness probe path | `/api/v1/status` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `30` |
| `probes.liveness.periodSeconds` | Liveness period | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/api/v1/status` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `5` |
| `probes.readiness.periodSeconds` | Readiness period | `5` |
## Troubleshooting
- **Application not starting**: Check that persistence is enabled and the PVC is accessible
- **Timezone issues**: Set the `TZ` environment variable to your local timezone
```bash
kubectl logs deployment/jellyseerr -f
kubectl describe pod -l app.kubernetes.io/name=jellyseerr
```
## Links
- [Jellyseerr GitHub](https://github.com/fallenbagel/jellyseerr)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/jellyseerr)

View File

@ -1,266 +1,40 @@
# Joplin Server Helm Chart # Joplin Server Helm Chart
A Helm chart for deploying Joplin Server on Kubernetes - Note-taking and synchronization server. A Helm chart for deploying [Joplin Server](https://github.com/laurent22/joplin) on Kubernetes.
## Introduction ## Introduction
This chart deploys [Joplin Server](https://github.com/laurent22/joplin) on a Kubernetes cluster using the Helm package manager. Joplin Server is the synchronization server for Joplin, allowing you to sync your notes across devices. This chart deploys Joplin Server, the synchronization server for the Joplin note-taking application, on a Kubernetes cluster. Joplin Server allows syncing notes across devices and supports filesystem or S3 storage, email notifications, and an optional AI transcription service.
Source code can be found here: Source code: https://github.com/rtomik/helm-charts/tree/main/charts/joplin-server
- https://github.com/rtomik/helm-charts/tree/main/charts/joplin-server
## Prerequisites ## Prerequisites
- Kubernetes 1.19+ - Kubernetes 1.19+
- Helm 3.0+ - Helm 3.0+
- **External PostgreSQL database** (Required - Joplin Server does not support SQLite in production) - **External PostgreSQL database** (required Joplin Server does not support SQLite)
- PV provisioner support in the underlying infrastructure (if persistence is needed for file storage) - PV provisioner support (if using filesystem storage)
## Installing the Chart ## Installing the Chart
To install the chart with the release name `joplin-server`:
```bash ```bash
$ helm repo add joplin-chart https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
$ helm install joplin-server joplin-chart/joplin-server helm install joplin-server rtomik/joplin-server
``` ```
> **Important**: You must configure PostgreSQL database settings before installation. > **Important**: Configure PostgreSQL database settings before installation.
## Uninstalling the Chart ## Uninstalling the Chart
To uninstall/delete the `joplin-server` deployment:
```bash ```bash
$ helm uninstall joplin-server helm uninstall joplin-server
``` ```
## Parameters
### Global parameters
| Name | Description | Value |
|------------------------|------------------------------------------------|-------|
| `nameOverride` | String to partially override the release name | `""` |
| `fullnameOverride` | String to fully override the release name | `""` |
### Image parameters
| Name | Description | Value |
|-------------------------|-----------------------------------|--------------------|
| `image.repository` | Joplin Server image repository | `joplin/server` |
| `image.tag` | Joplin Server image tag | `latest` |
| `image.pullPolicy` | Joplin Server image pull policy | `IfNotPresent` |
### Deployment parameters
| Name | Description | Value |
|--------------------------------------|-----------------------------------------------|-----------|
| `replicaCount` | Number of Joplin Server replicas | `1` |
| `revisionHistoryLimit` | Number of revisions to retain for rollback | `3` |
| `podSecurityContext.runAsNonRoot` | Run containers as non-root user | `true` |
| `podSecurityContext.runAsUser` | User ID for the container | `1001` |
| `podSecurityContext.fsGroup` | Group ID for the container filesystem | `1001` |
| `containerSecurityContext` | Security context for the container | See values.yaml |
| `nodeSelector` | Node labels for pod assignment | `{}` |
| `tolerations` | Tolerations for pod assignment | `[]` |
| `affinity` | Affinity for pod assignment | `{}` |
### Service parameters
| Name | Description | Value |
|----------------|-----------------------|-------------|
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `22300` |
### Ingress parameters
| Name | Description | Value |
|-------------------------|-------------------------------------------|-----------------|
| `ingress.enabled` | Enable ingress record generation | `false` |
| `ingress.className` | IngressClass name | `""` |
| `ingress.annotations` | Additional annotations for the Ingress | See values.yaml |
| `ingress.hosts` | Array of host and path objects | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### Environment variables
| Name | Description | Value |
|---------------------------|-----------------------------------------------|--------------------------|
| `env.APP_PORT` | Application port | `22300` |
| `env.APP_BASE_URL` | Base URL for the application | `http://localhost:22300` |
| `env.DB_CLIENT` | Database client (always pg for PostgreSQL) | `pg` |
### PostgreSQL configuration (Required)
| Name | Description | Value |
|----------------------------------------|-----------------------------------------------|-----------|
| `postgresql.external.enabled` | Use external PostgreSQL database (required) | `true` |
| `postgresql.external.host` | PostgreSQL host | `""` |
| `postgresql.external.port` | PostgreSQL port | `5432` |
| `postgresql.external.database` | PostgreSQL database name | `joplin` |
| `postgresql.external.user` | PostgreSQL username | `joplin` |
| `postgresql.external.password` | PostgreSQL password | `""` |
| `postgresql.external.existingSecret` | Name of existing secret with PostgreSQL credentials | `""` |
| `postgresql.external.userKey` | Key in the secret for username | `username` |
| `postgresql.external.passwordKey` | Key in the secret for password | `password` |
| `postgresql.external.hostKey` | Key in the secret for host (optional) | `""` |
| `postgresql.external.portKey` | Key in the secret for port (optional) | `""` |
| `postgresql.external.databaseKey` | Key in the secret for database name (optional) | `""` |
### Joplin Server Configuration
#### Admin Settings
| Name | Description | Value |
|----------------------------------------|-----------------------------------------------|-----------|
| `joplin.admin.email` | First admin user email | `""` |
| `joplin.admin.password` | First admin user password | `""` |
| `joplin.admin.existingSecret` | Name of existing secret with admin credentials | `""` |
| `joplin.admin.emailKey` | Key in the secret for admin email | `admin-email` |
| `joplin.admin.passwordKey` | Key in the secret for admin password | `admin-password` |
#### Server Settings
| Name | Description | Value |
|-------------------------------------------|-----------------------------------------------|-----------|
| `joplin.server.maxRequestBodySize` | Maximum request body size | `200mb` |
| `joplin.server.sessionTimeout` | Session timeout in seconds | `86400` |
| `joplin.server.enableUserRegistration` | Enable/disable user registration | `false` |
| `joplin.server.enableSharing` | Enable/disable sharing | `true` |
| `joplin.server.enablePublicNotes` | Enable/disable public notes | `true` |
#### Storage Settings
| Name | Description | Value |
|-------------------------------------------|-----------------------------------------------|--------------|
| `joplin.storage.driver` | Storage driver (filesystem, s3, azure) | `filesystem` |
| `joplin.storage.filesystemPath` | Path for filesystem storage | `/var/lib/joplin` |
##### S3 Storage (Optional)
| Name | Description | Value |
|---------------------------------------------|---------------------------------------------|-----------|
| `joplin.storage.s3.bucket` | S3 bucket name | `""` |
| `joplin.storage.s3.region` | S3 region | `""` |
| `joplin.storage.s3.accessKeyId` | S3 access key ID | `""` |
| `joplin.storage.s3.secretAccessKey` | S3 secret access key | `""` |
| `joplin.storage.s3.endpoint` | S3 endpoint (for S3-compatible services) | `""` |
| `joplin.storage.s3.existingSecret` | Name of existing secret with S3 credentials | `""` |
| `joplin.storage.s3.accessKeyIdKey` | Key in the secret for access key ID | `access-key-id` |
| `joplin.storage.s3.secretAccessKeyKey` | Key in the secret for secret access key | `secret-access-key` |
#### Email Settings (Optional)
| Name | Description | Value |
|----------------------------------------|-----------------------------------------------|-----------|
| `joplin.email.enabled` | Enable email notifications | `false` |
| `joplin.email.host` | SMTP host | `""` |
| `joplin.email.port` | SMTP port | `587` |
| `joplin.email.username` | SMTP username | `""` |
| `joplin.email.password` | SMTP password | `""` |
| `joplin.email.fromEmail` | From email address | `""` |
| `joplin.email.fromName` | From name | `Joplin Server` |
| `joplin.email.secure` | Use TLS/SSL | `true` |
| `joplin.email.existingSecret` | Name of existing secret with email credentials | `""` |
| `joplin.email.usernameKey` | Key in the secret for SMTP username | `email-username` |
| `joplin.email.passwordKey` | Key in the secret for SMTP password | `email-password` |
#### Logging Settings
| Name | Description | Value |
|--------------------------------|--------------------------------------|-----------|
| `joplin.logging.level` | Log level (error, warn, info, debug) | `info` |
| `joplin.logging.target` | Log target (console, file) | `console` |
### Persistence settings (for filesystem storage)
| Name | Description | Value |
|-------------------------------|----------------------------------|-----------------|
| `persistence.enabled` | Enable persistence using PVC | `true` |
| `persistence.storageClass` | PVC Storage Class | `""` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` | PVC Size | `10Gi` |
| `persistence.annotations` | Annotations for PVC | `{}` |
### Transcribe Service (Optional AI Transcription)
| Name | Description | Value |
|-------------------------------------------|-----------------------------------------------|--------------|
| `transcribe.enabled` | Enable transcribe service | `false` |
| `transcribe.image.repository` | Transcribe image repository | `joplin/transcribe` |
| `transcribe.image.tag` | Transcribe image tag | `latest` |
| `transcribe.image.pullPolicy` | Transcribe image pull policy | `IfNotPresent` |
| `transcribe.api.key` | Shared secret between Joplin and Transcribe | `""` |
| `transcribe.api.existingSecret` | Name of existing secret with transcribe API key | `""` |
| `transcribe.api.keyName` | Key in the secret for transcribe API key | `transcribe-api-key` |
| `transcribe.service.type` | Transcribe service type | `ClusterIP` |
| `transcribe.service.port` | Transcribe service port | `4567` |
| `transcribe.htr.imagesFolder` | HTR images folder path | `/app/images` |
#### Transcribe Database (Separate from main database)
| Name | Description | Value |
|---------------------------------------------|---------------------------------------------|-------------|
| `transcribe.database.host` | Transcribe database host | `""` |
| `transcribe.database.port` | Transcribe database port | `5432` |
| `transcribe.database.database` | Transcribe database name | `transcribe` |
| `transcribe.database.user` | Transcribe database username | `transcribe` |
| `transcribe.database.password` | Transcribe database password | `""` |
| `transcribe.database.existingSecret` | Name of existing secret with transcribe DB credentials | `""` |
| `transcribe.database.userKey` | Key in the secret for username | `username` |
| `transcribe.database.passwordKey` | Key in the secret for password | `password` |
### Resource Configuration
| Name | Description | Value |
|-------------|--------------------------------------|-------|
| `resources` | Resource limits and requests | `{}` |
### Health Checks
| Name | Description | Value |
|-------------------------------------------|------------------------------------------|-------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.initialDelaySeconds` | Initial delay for liveness probe | `60` |
| `probes.liveness.periodSeconds` | Period for liveness probe | `30` |
| `probes.liveness.timeoutSeconds` | Timeout for liveness probe | `10` |
| `probes.liveness.failureThreshold` | Failure threshold for liveness probe | `3` |
| `probes.liveness.successThreshold` | Success threshold for liveness probe | `1` |
| `probes.liveness.path` | Path for liveness probe | `/api/ping` |
| `probes.liveness.httpHeaders` | HTTP headers for liveness probe | `[{"name": "Host", "value": "joplin.domain.com"}]` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.initialDelaySeconds` | Initial delay for readiness probe | `30` |
| `probes.readiness.periodSeconds` | Period for readiness probe | `10` |
| `probes.readiness.timeoutSeconds` | Timeout for readiness probe | `5` |
| `probes.readiness.failureThreshold` | Failure threshold for readiness probe | `3` |
| `probes.readiness.successThreshold` | Success threshold for readiness probe | `1` |
| `probes.readiness.path` | Path for readiness probe | `/api/ping` |
| `probes.readiness.httpHeaders` | HTTP headers for readiness probe | `[{"name": "Host", "value": "joplin.domain.com"}]` |
### Autoscaling
| Name | Description | Value |
|---------------------------------------------|------------------------------------------|---------|
| `autoscaling.enabled` | Enable horizontal pod autoscaling | `false` |
| `autoscaling.minReplicas` | Minimum number of replicas | `1` |
| `autoscaling.maxReplicas` | Maximum number of replicas | `3` |
| `autoscaling.targetCPUUtilizationPercentage`| Target CPU utilization percentage | `80` |
| `autoscaling.targetMemoryUtilizationPercentage`| Target memory utilization percentage | `80` |
### Security Settings
| Name | Description | Value |
|-----------------------------------|--------------------------------------|---------|
| `security.httpsRedirect` | Enable/disable HTTPS redirect | `false` |
| `security.tls.enabled` | Enable custom TLS certificate | `false` |
| `security.tls.existingSecret` | Name of existing secret with TLS cert| `""` |
| `security.tls.certificateKey` | Key in the secret for TLS certificate| `tls.crt` |
| `security.tls.privateKeyKey` | Key in the secret for TLS private key| `tls.key` |
## Configuration Examples ## Configuration Examples
### Basic Installation with PostgreSQL ### Minimal Installation
> **Important**: Health check probes require a `Host` header matching your ingress domain. Update `probes.*.httpHeaders` accordingly.
```yaml ```yaml
postgresql: postgresql:
@ -272,22 +46,9 @@ postgresql:
user: "joplin" user: "joplin"
password: "secure-password" password: "secure-password"
ingress:
enabled: true
hosts:
- host: joplin.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- joplin.example.com
secretName: joplin-tls
env: env:
APP_BASE_URL: "https://joplin.example.com" APP_BASE_URL: "https://joplin.example.com"
# IMPORTANT: Update health check host headers to match your domain
probes: probes:
liveness: liveness:
httpHeaders: httpHeaders:
@ -304,30 +65,22 @@ joplin:
password: "admin-password" password: "admin-password"
server: server:
enableUserRegistration: true enableUserRegistration: true
```
### Using Kubernetes Secrets ingress:
#### Full Secret Configuration
```yaml
postgresql:
external:
enabled: true enabled: true
existingSecret: "joplin-postgresql-secret" hosts:
hostKey: "host" - host: joplin.example.com
portKey: "port" paths:
databaseKey: "database" - path: /
userKey: "username" pathType: Prefix
passwordKey: "password" tls:
- hosts:
joplin: - joplin.example.com
admin: secretName: joplin-tls
existingSecret: "joplin-admin-secret"
emailKey: "email"
passwordKey: "password"
``` ```
#### Mixed Configuration (Host in values, credentials in secret) ### Production with Existing Secrets
```yaml ```yaml
postgresql: postgresql:
external: external:
@ -338,10 +91,15 @@ postgresql:
existingSecret: "joplin-db-credentials" existingSecret: "joplin-db-credentials"
userKey: "username" userKey: "username"
passwordKey: "password" passwordKey: "password"
# hostKey, portKey, databaseKey left empty - using values above
joplin:
admin:
existingSecret: "joplin-admin-secret"
emailKey: "email"
passwordKey: "password"
``` ```
### S3 Storage Configuration ### S3 Storage
```yaml ```yaml
joplin: joplin:
@ -359,7 +117,7 @@ persistence:
enabled: false enabled: false
``` ```
### Email Notifications Setup ### Email Notifications
```yaml ```yaml
joplin: joplin:
@ -375,7 +133,7 @@ joplin:
passwordKey: "password" passwordKey: "password"
``` ```
### Transcribe Service (AI Features) ### Transcribe Service (AI Transcription)
```yaml ```yaml
transcribe: transcribe:
@ -396,32 +154,206 @@ transcribe:
size: 5Gi size: 5Gi
``` ```
## First-time Setup ## Parameters
1. **Configure PostgreSQL**: Ensure your PostgreSQL database is accessible and credentials are configured ### Global Parameters
2. **Admin User**: Set admin email/password or access the web interface to create the first admin user
3. **User Registration**: Configure whether users can self-register or admin approval is required
4. **Storage**: Choose between filesystem (requires persistence) or cloud storage (S3/Azure)
## Security Considerations | Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
For production deployments: ### Image Parameters
1. Use external secrets for all sensitive information (database passwords, admin credentials, etc.) | Name | Description | Default |
2. Enable TLS/SSL for all communications |------|-------------|---------|
3. Configure proper RBAC and network policies | `image.repository` | Joplin Server image repository | `joplin/server` |
4. Use dedicated databases with proper access controls | `image.tag` | Image tag | `3.4.2` |
5. Disable user registration if not needed | `image.pullPolicy` | Image pull policy | `IfNotPresent` |
6. Use cloud storage for better scalability and backup
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `podSecurityContext.runAsNonRoot` | Run as non-root | `true` |
| `podSecurityContext.runAsUser` | User ID | `1001` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `1001` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `22300` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### PostgreSQL Configuration (Required)
| Name | Description | Default |
|------|-------------|---------|
| `postgresql.external.enabled` | Use external PostgreSQL | `true` |
| `postgresql.external.host` | PostgreSQL host | `""` |
| `postgresql.external.port` | PostgreSQL port | `5432` |
| `postgresql.external.database` | Database name | `joplin` |
| `postgresql.external.user` | Username | `joplin` |
| `postgresql.external.password` | Password | `""` |
| `postgresql.external.existingSecret` | Existing secret name | `""` |
| `postgresql.external.userKey` | Key for username in secret | `username` |
| `postgresql.external.passwordKey` | Key for password in secret | `password` |
| `postgresql.external.hostKey` | Key for host in secret (optional) | `""` |
| `postgresql.external.portKey` | Key for port in secret (optional) | `""` |
| `postgresql.external.databaseKey` | Key for database in secret (optional) | `""` |
### Admin Settings
| Name | Description | Default |
|------|-------------|---------|
| `joplin.admin.email` | Admin user email | `""` |
| `joplin.admin.password` | Admin user password | `""` |
| `joplin.admin.existingSecret` | Existing secret for admin credentials | `""` |
| `joplin.admin.emailKey` | Key for email in secret | `admin-email` |
| `joplin.admin.passwordKey` | Key for password in secret | `admin-password` |
### Server Settings
| Name | Description | Default |
|------|-------------|---------|
| `joplin.server.maxRequestBodySize` | Max request body size | `200mb` |
| `joplin.server.sessionTimeout` | Session timeout (seconds) | `86400` |
| `joplin.server.enableUserRegistration` | Enable user registration | `false` |
| `joplin.server.enableSharing` | Enable sharing | `true` |
| `joplin.server.enablePublicNotes` | Enable public notes | `true` |
### Storage Settings
| Name | Description | Default |
|------|-------------|---------|
| `joplin.storage.driver` | Storage driver (`filesystem`, `s3`, `azure`) | `filesystem` |
| `joplin.storage.filesystemPath` | Filesystem storage path | `/var/lib/joplin` |
| `joplin.storage.s3.bucket` | S3 bucket name | `""` |
| `joplin.storage.s3.region` | S3 region | `""` |
| `joplin.storage.s3.endpoint` | S3 endpoint (for S3-compatible services) | `""` |
| `joplin.storage.s3.accessKeyId` | S3 access key ID | `""` |
| `joplin.storage.s3.secretAccessKey` | S3 secret access key | `""` |
| `joplin.storage.s3.existingSecret` | Existing secret for S3 credentials | `""` |
| `joplin.storage.s3.accessKeyIdKey` | Key for access key in secret | `access-key-id` |
| `joplin.storage.s3.secretAccessKeyKey` | Key for secret access key in secret | `secret-access-key` |
### Email Settings
| Name | Description | Default |
|------|-------------|---------|
| `joplin.email.enabled` | Enable email | `false` |
| `joplin.email.host` | SMTP host | `""` |
| `joplin.email.port` | SMTP port | `587` |
| `joplin.email.username` | SMTP username | `""` |
| `joplin.email.password` | SMTP password | `""` |
| `joplin.email.fromEmail` | From email address | `""` |
| `joplin.email.fromName` | From name | `Joplin Server` |
| `joplin.email.secure` | Use TLS/SSL | `true` |
| `joplin.email.existingSecret` | Existing secret for credentials | `""` |
| `joplin.email.usernameKey` | Key for username in secret | `email-username` |
| `joplin.email.passwordKey` | Key for password in secret | `email-password` |
### Logging Settings
| Name | Description | Default |
|------|-------------|---------|
| `joplin.logging.level` | Log level (`error`, `warn`, `info`, `debug`) | `info` |
| `joplin.logging.target` | Log target (`console`, `file`) | `console` |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.enabled` | Enable persistence | `true` |
| `persistence.storageClass` | Storage class | `""` |
| `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | PVC size | `10Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Transcribe Service
| Name | Description | Default |
|------|-------------|---------|
| `transcribe.enabled` | Enable transcribe service | `false` |
| `transcribe.image.repository` | Transcribe image repository | `joplin/transcribe` |
| `transcribe.image.tag` | Transcribe image tag | `latest` |
| `transcribe.api.key` | Shared API key | `""` |
| `transcribe.api.existingSecret` | Existing secret for API key | `""` |
| `transcribe.api.keyName` | Key name in secret | `transcribe-api-key` |
| `transcribe.service.type` | Transcribe service type | `ClusterIP` |
| `transcribe.service.port` | Transcribe service port | `4567` |
| `transcribe.database.host` | Transcribe DB host | `""` |
| `transcribe.database.port` | Transcribe DB port | `5432` |
| `transcribe.database.database` | Transcribe DB name | `transcribe` |
| `transcribe.database.user` | Transcribe DB user | `transcribe` |
| `transcribe.database.password` | Transcribe DB password | `""` |
| `transcribe.database.existingSecret` | Existing secret for transcribe DB | `""` |
| `transcribe.database.userKey` | Key for username in secret | `username` |
| `transcribe.database.passwordKey` | Key for password in secret | `password` |
### Security Settings
| Name | Description | Default |
|------|-------------|---------|
| `security.httpsRedirect` | Enable HTTPS redirect | `false` |
| `security.tls.enabled` | Enable custom TLS certificate | `false` |
| `security.tls.existingSecret` | Secret with TLS certificate | `""` |
| `security.tls.certificateKey` | Key for TLS certificate in secret | `tls.crt` |
| `security.tls.privateKeyKey` | Key for TLS private key in secret | `tls.key` |
### Resource Parameters
| Name | Description | Default |
|------|-------------|---------|
| `resources` | Resource limits and requests | `{}` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Liveness probe path | `/api/ping` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `60` |
| `probes.liveness.periodSeconds` | Liveness period | `30` |
| `probes.liveness.httpHeaders` | Liveness HTTP headers | Host matching ingress |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/api/ping` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `30` |
| `probes.readiness.periodSeconds` | Readiness period | `10` |
| `probes.readiness.httpHeaders` | Readiness HTTP headers | Host matching ingress |
### Autoscaling Parameters
| Name | Description | Default |
|------|-------------|---------|
| `autoscaling.enabled` | Enable HPA | `false` |
| `autoscaling.minReplicas` | Min replicas | `1` |
| `autoscaling.maxReplicas` | Max replicas | `3` |
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU | `80` |
| `autoscaling.targetMemoryUtilizationPercentage` | Target memory | `80` |
## Troubleshooting ## Troubleshooting
Common issues and solutions: ### Health Check Failures / "No Available Server"
Health checks require the correct `Host` header matching your ingress domain:
1. **Health Check Issues / "No Available Server"**:
- Ensure `probes.*.httpHeaders` includes the correct Host header matching your domain
- Health checks use `/api/ping` endpoint which requires proper host validation
- Example fix:
```yaml ```yaml
probes: probes:
liveness: liveness:
@ -434,27 +366,22 @@ Common issues and solutions:
value: your-joplin-domain.com value: your-joplin-domain.com
``` ```
2. **Database connection issues**: Verify PostgreSQL credentials and network connectivity ### Database Connection Issues
3. **Storage permissions**: Check filesystem permissions for persistent volumes
4. **First admin user**: If no admin configured, access the web interface to create one
5. **Transcribe issues**: Verify Docker socket access and separate database configuration
6. **Origin validation errors**: Make sure `env.APP_BASE_URL` matches your ingress host
For detailed troubleshooting, check the application logs: Verify PostgreSQL credentials, network connectivity, and that `env.APP_BASE_URL` matches your ingress host.
### Origin Validation Errors
Ensure `env.APP_BASE_URL` matches your ingress hostname exactly.
### Debugging
```bash ```bash
kubectl logs -f deployment/joplin-server kubectl logs -f deployment/joplin-server
```
Check pod status and events:
```bash
kubectl describe pod -l app.kubernetes.io/name=joplin-server kubectl describe pod -l app.kubernetes.io/name=joplin-server
``` ```
## Backing Up ## Links
- **Database**: Use PostgreSQL backup tools (pg_dump, etc.) - [Joplin GitHub](https://github.com/laurent22/joplin)
- **File Storage**: - [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/joplin-server)
- Filesystem: Backup the PVC data
- S3: Files are already stored in S3 (ensure proper S3 backup policies)
- **Configuration**: Backup your Kubernetes secrets and config

View File

@ -1,14 +1,18 @@
# Karakeep Helm Chart # Karakeep Helm Chart
This Helm chart deploys [Karakeep](https://github.com/karakeep-app/karakeep), a bookmark management application, along with its required services on a Kubernetes cluster. A Helm chart for deploying [Karakeep](https://github.com/karakeep-app/karakeep), a bookmark management application, on Kubernetes.
## Components ## Introduction
This chart deploys three containers in a single pod: This chart deploys Karakeep as a multi-container pod with three services:
1. **Karakeep**: The main bookmark management application 1. **Karakeep** — Main bookmark management application
2. **Chrome**: Headless Chrome browser for web scraping and preview generation 2. **Chrome** Headless browser for web scraping and preview generation
3. **MeiliSearch**: Search engine for fast bookmark search functionality 3. **MeiliSearch** Search engine for fast bookmark search
All containers share the same pod network and communicate via localhost.
Source code: https://github.com/rtomik/helm-charts/tree/main/charts/karakeep
## Prerequisites ## Prerequisites
@ -18,91 +22,180 @@ This chart deploys three containers in a single pod:
## Installing the Chart ## Installing the Chart
To install the chart with the release name `karakeep`:
```bash ```bash
helm repo add karakeep-chart https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
helm install karakeep karakeep-chart/karakeep helm install karakeep rtomik/karakeep
``` ```
## Uninstalling the Chart ## Uninstalling the Chart
To uninstall/delete the `karakeep` deployment:
```bash ```bash
helm delete karakeep helm uninstall karakeep
``` ```
## Configuration ## Configuration Examples
The following table lists the configurable parameters and their default values. ### Minimal Installation
### Global Settings ```yaml
ingress:
enabled: true
hosts:
- host: karakeep.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- karakeep.example.com
```
| Parameter | Description | Default | ### Production with Secrets
|-----------|-------------|---------|
| `nameOverride` | Override the name of the chart | `""` | For production, store `NEXTAUTH_SECRET` in a Kubernetes secret. When ingress is enabled, `NEXTAUTH_URL` is automatically set to the ingress hostname.
| `fullnameOverride` | Override the full name of the chart | `""` |
```yaml
secrets:
create: true
env:
NEXTAUTH_SECRET: "your-secure-32-character-string"
ingress:
enabled: true
hosts:
- host: karakeep.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- karakeep.example.com
```
### With OpenAI Integration
```yaml
secrets:
create: true
env:
NEXTAUTH_SECRET: "your-secure-32-character-string"
OPENAI_API_KEY: "your-openai-api-key"
```
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the chart name | `""` |
| `fullnameOverride` | Override the full chart name | `""` |
| `replicaCount` | Number of replicas | `1` | | `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
### Karakeep Configuration ### Pod Security Parameters
| Parameter | Description | Default | | Name | Description | Default |
|-----------|-------------|---------| |------|-------------|---------|
| `podSecurityContext.runAsNonRoot` | Run as non-root | `false` |
| `podSecurityContext.runAsUser` | User ID | `0` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `0` |
### Karakeep Parameters
| Name | Description | Default |
|------|-------------|---------|
| `karakeep.image.repository` | Karakeep image repository | `ghcr.io/karakeep-app/karakeep` | | `karakeep.image.repository` | Karakeep image repository | `ghcr.io/karakeep-app/karakeep` |
| `karakeep.image.tag` | Karakeep image tag | `"release"` | | `karakeep.image.tag` | Karakeep image tag | `0.26.0` |
| `karakeep.image.pullPolicy` | Image pull policy | `IfNotPresent` | | `karakeep.image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `karakeep.service.port` | Karakeep service port | `3000` |
| `karakeep.env` | Karakeep environment variables | See values.yaml |
| `karakeep.extraEnv` | Additional environment variables | `[]` |
### Chrome Configuration ### Chrome Parameters
| Parameter | Description | Default | | Name | Description | Default |
|-----------|-------------|---------| |------|-------------|---------|
| `chrome.image.repository` | Chrome image repository | `gcr.io/zenika-hub/alpine-chrome` | | `chrome.image.repository` | Chrome image repository | `gcr.io/zenika-hub/alpine-chrome` |
| `chrome.image.tag` | Chrome image tag | `"124"` | | `chrome.image.tag` | Chrome image tag | `124` |
| `chrome.image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `chrome.service.port` | Chrome debugging port | `9222` |
### MeiliSearch Configuration ### MeiliSearch Parameters
| Parameter | Description | Default | | Name | Description | Default |
|-----------|-------------|---------| |------|-------------|---------|
| `meilisearch.image.repository` | MeiliSearch image repository | `getmeili/meilisearch` | | `meilisearch.image.repository` | MeiliSearch image repository | `getmeili/meilisearch` |
| `meilisearch.image.tag` | MeiliSearch image tag | `"v1.13.3"` | | `meilisearch.image.tag` | MeiliSearch image tag | `v1.13.3` |
| `meilisearch.image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `meilisearch.service.port` | MeiliSearch port | `7700` |
| `meilisearch.resources.limits.cpu` | CPU limit | `500m` |
| `meilisearch.resources.limits.memory` | Memory limit | `1Gi` |
| `meilisearch.resources.requests.cpu` | CPU request | `100m` |
| `meilisearch.resources.requests.memory` | Memory request | `256Mi` |
### Persistence ### Service Parameters
| Parameter | Description | Default | | Name | Description | Default |
|-----------|-------------|---------| |------|-------------|---------|
| `persistence.enabled` | Enable persistent storage | `true` | | `service.type` | Service type | `ClusterIP` |
| `persistence.data.size` | Size of data volume | `5Gi` | | `service.port` | Service port | `3000` |
| `persistence.data.storageClass` | Storage class for data volume | `""` |
| `persistence.meilisearch.size` | Size of MeiliSearch volume | `2Gi` |
| `persistence.meilisearch.storageClass` | Storage class for MeiliSearch volume | `""` |
### Ingress ### Ingress Parameters
| Parameter | Description | Default | | Name | Description | Default |
|-----------|-------------|---------| |------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` | | `ingress.enabled` | Enable ingress | `false` |
| `ingress.hosts[0].host` | Hostname | `karakeep.domain.com` | | `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### Secrets ### Persistence Parameters
| Parameter | Description | Default | | Name | Description | Default |
|-----------|-------------|---------| |------|-------------|---------|
| `persistence.enabled` | Enable persistence | `true` |
| `persistence.data.storageClass` | Data volume storage class | `""` |
| `persistence.data.accessMode` | Data volume access mode | `ReadWriteOnce` |
| `persistence.data.size` | Data volume size | `5Gi` |
| `persistence.meilisearch.storageClass` | MeiliSearch volume storage class | `""` |
| `persistence.meilisearch.accessMode` | MeiliSearch volume access mode | `ReadWriteOnce` |
| `persistence.meilisearch.size` | MeiliSearch volume size | `2Gi` |
### Secret Parameters
| Name | Description | Default |
|------|-------------|---------|
| `secrets.create` | Create secret for environment variables | `false` | | `secrets.create` | Create secret for environment variables | `false` |
| `secrets.existingSecret` | Use existing secret | `""` | | `secrets.existingSecret` | Use an existing secret | `""` |
| `secrets.env` | Environment variables to store in secret | `{}` | | `secrets.env` | Environment variables for the secret | `{}` |
**Important Configuration:** ## Troubleshooting
1. The default `NEXTAUTH_SECRET` is set to a placeholder value. For production deployments, you should either:
- Override the value: `--set karakeep.env[3].value="your-secure-32-character-string"`
- Use secrets: `--set secrets.create=true --set secrets.env.NEXTAUTH_SECRET="your-secure-32-character-string"`
2. When ingress is enabled, `NEXTAUTH_URL` is automatically set to the ingress hostname. For custom configurations: ### NEXTAUTH_SECRET Not Set
- Override manually: `--set karakeep.env[4].value="https://your-domain.com"`
## Notes The default `NEXTAUTH_SECRET` is a placeholder. For production, override it:
- This chart creates a multi-container pod with all three services running together ```yaml
- Data persistence is enabled by default with separate volumes for Karakeep data and MeiliSearch indices secrets:
- The services communicate via localhost since they share the same pod network create: true
- Chrome runs with security flags for containerized environments env:
NEXTAUTH_SECRET: "your-secure-32-character-string"
```
### Custom NEXTAUTH_URL
If not using ingress or using a custom domain, override `NEXTAUTH_URL` manually:
```yaml
karakeep:
env:
- name: NEXTAUTH_URL
value: "https://your-domain.com"
```
## Links
- [Karakeep GitHub](https://github.com/karakeep-app/karakeep)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/karakeep)

View File

@ -1,273 +1,41 @@
# Mealie Helm Chart # Mealie Helm Chart
A Helm chart for deploying Mealie recipe management and meal planning application on Kubernetes. A Helm chart for deploying [Mealie](https://github.com/mealie-recipes/mealie), a recipe management and meal planning application, on Kubernetes.
## Introduction ## Introduction
This chart deploys [Mealie](https://github.com/mealie-recipes/mealie) on a Kubernetes cluster using the Helm package manager. Mealie is a self-hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. This chart deploys Mealie on a Kubernetes cluster using the Helm package manager. Mealie is a self-hosted recipe manager with a reactive frontend and RestAPI backend, supporting PostgreSQL or SQLite databases, LDAP/OIDC authentication, and OpenAI integration.
Source code can be found here: Source code: https://github.com/rtomik/helm-charts/tree/main/charts/mealie
- https://github.com/rtomik/helm-charts/tree/main/charts/mealie
## Prerequisites ## Prerequisites
- Kubernetes 1.19+ - Kubernetes 1.19+
- Helm 3.0+ - Helm 3.0+
- PV provisioner support in the underlying infrastructure (if persistence is needed) - External PostgreSQL database (recommended, e.g. [CloudNativePG](https://cloudnative-pg.io/))
- External Postgresql DB like https://cloudnative-pg.io/ - PV provisioner support (if persistence is needed)
## Installing the Chart ## Installing the Chart
To install the chart with the release name `mealie`:
```bash ```bash
$ helm repo add mealie-chart https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
$ helm install mealie mealie-chart/mealie helm install mealie rtomik/mealie
``` ```
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart ## Uninstalling the Chart
To uninstall/delete the `mealie` deployment:
```bash ```bash
$ helm uninstall mealie helm uninstall mealie
``` ```
## Parameters
### Global parameters
| Name | Description | Value |
|------------------------|------------------------------------------------|-------|
| `nameOverride` | String to partially override the release name | `""` |
| `fullnameOverride` | String to fully override the release name | `""` |
### Image parameters
| Name | Description | Value |
|-------------------------|-----------------------------------|-----------------------------------|
| `image.repository` | Mealie image repository | `ghcr.io/mealie-recipes/mealie` |
| `image.tag` | Mealie image tag | `v3.1.1` |
| `image.pullPolicy` | Mealie image pull policy | `IfNotPresent` |
### Deployment parameters
| Name | Description | Value |
|--------------------------------------|-----------------------------------------------|-----------|
| `replicaCount` | Number of Mealie replicas | `1` |
| `revisionHistoryLimit` | Number of revisions to retain for rollback | `3` |
| `podSecurityContext.runAsNonRoot` | Run containers as non-root user | `false` |
| `podSecurityContext.runAsUser` | User ID for the container | `911` |
| `podSecurityContext.fsGroup` | Group ID for the container filesystem | `911` |
| `containerSecurityContext` | Security context for the container | See values.yaml |
| `nodeSelector` | Node labels for pod assignment | `{}` |
| `tolerations` | Tolerations for pod assignment | `[]` |
| `affinity` | Affinity for pod assignment | `{}` |
### Service parameters
| Name | Description | Value |
|----------------|-----------------------|-------------|
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `9000` |
### Ingress parameters
| Name | Description | Value |
|-------------------------|-------------------------------------------|-----------------|
| `ingress.enabled` | Enable ingress record generation | `false` |
| `ingress.className` | IngressClass name | `""` |
| `ingress.annotations` | Additional annotations for the Ingress | See values.yaml |
| `ingress.hosts` | Array of host and path objects | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### Persistence parameters
| Name | Description | Value |
|-------------------------------|----------------------------------|-----------------|
| `persistence.enabled` | Enable persistence using PVC | `true` |
| `persistence.storageClass` | PVC Storage Class | `""` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` | PVC Size | `5Gi` |
| `persistence.annotations` | Annotations for PVC | `{}` |
### Environment variables
| Name | Description | Value |
|---------------------------------------|-----------------------------------------------|-----------------|
| `env.PUID` | UserID permissions between host OS and container | `911` |
| `env.PGID` | GroupID permissions between host OS and container | `911` |
| `env.DEFAULT_GROUP` | The default group for users | `Home` |
| `env.DEFAULT_HOUSEHOLD` | The default household for users in each group | `Family` |
| `env.BASE_URL` | Used for Notifications | `http://localhost:9000` |
| `env.TOKEN_TIME` | The time in hours that a login/auth token is valid | `48` |
| `env.API_PORT` | The port exposed by backend API | `9000` |
| `env.API_DOCS` | Turns on/off access to the API documentation | `true` |
| `env.TZ` | Must be set to get correct date/time on the server | `UTC` |
| `env.ALLOW_SIGNUP` | Allow user sign-up without token | `false` |
| `env.ALLOW_PASSWORD_LOGIN` | Whether or not to display username+password input fields | `true` |
| `env.LOG_LEVEL` | Logging level | `info` |
| `env.DAILY_SCHEDULE_TIME` | Time to run daily server tasks (HH:MM) | `23:45` |
### PostgreSQL configuration
| Name | Description | Value |
|----------------------------------------|-----------------------------------------------|-----------|
| `postgresql.enabled` | Enable PostgreSQL support | `false` |
| `postgresql.external.enabled` | Use external PostgreSQL database | `false` |
| `postgresql.external.host` | PostgreSQL host | `""` |
| `postgresql.external.port` | PostgreSQL port | `5432` |
| `postgresql.external.database` | PostgreSQL database name | `mealie` |
| `postgresql.external.user` | PostgreSQL username | `mealie` |
| `postgresql.external.password` | PostgreSQL password | `""` |
| `postgresql.external.existingSecret` | Name of existing secret with PostgreSQL credentials | `""` |
| `postgresql.external.userKey` | Key in the secret for username | `username` |
| `postgresql.external.passwordKey` | Key in the secret for password | `password` |
### Email (SMTP) configuration
| Name | Description | Value |
|--------------------------|--------------------------------------|-----------|
| `email.enabled` | Enable SMTP email support | `false` |
| `email.host` | SMTP host | `""` |
| `email.port` | SMTP port | `587` |
| `email.fromName` | From name for emails | `Mealie` |
| `email.authStrategy` | SMTP auth strategy (TLS, SSL, NONE) | `TLS` |
| `email.fromEmail` | From email address | `""` |
| `email.user` | SMTP username | `""` |
| `email.password` | SMTP password | `""` |
| `email.existingSecret` | Name of existing secret with SMTP credentials | `""` |
| `email.userKey` | Key in the secret for SMTP username | `smtp-user` |
| `email.passwordKey` | Key in the secret for SMTP password | `smtp-password` |
### LDAP Authentication
| Name | Description | Value |
|--------------------------|--------------------------------------|-----------|
| `ldap.enabled` | Enable LDAP authentication | `false` |
| `ldap.serverUrl` | LDAP server URL | `""` |
| `ldap.tlsInsecure` | Do not verify server certificate | `false` |
| `ldap.tlsCaCertFile` | Path to CA certificate file | `""` |
| `ldap.enableStartTls` | Use STARTTLS to connect to server | `false` |
| `ldap.baseDn` | Starting point for user authentication | `""` |
| `ldap.queryBind` | Optional bind user for LDAP searches | `""` |
| `ldap.queryPassword` | Password for the bind user | `""` |
| `ldap.userFilter` | LDAP filter to narrow down eligible users | `""` |
| `ldap.adminFilter` | LDAP filter for admin users | `""` |
| `ldap.idAttribute` | LDAP attribute for user ID | `uid` |
| `ldap.nameAttribute` | LDAP attribute for user name | `name` |
| `ldap.mailAttribute` | LDAP attribute for user email | `mail` |
### OpenID Connect (OIDC)
| Name | Description | Value |
|------------------------------|------------------------------------------|-----------|
| `oidc.enabled` | Enable OIDC authentication | `false` |
| `oidc.signupEnabled` | Allow new users via OIDC | `true` |
| `oidc.configurationUrl` | URL to OIDC configuration | `""` |
| `oidc.clientId` | OIDC client ID | `""` |
| `oidc.clientSecret` | OIDC client secret | `""` |
| `oidc.userGroup` | Required OIDC user group | `""` |
| `oidc.adminGroup` | OIDC admin group | `""` |
| `oidc.autoRedirect` | Bypass login page and redirect to IdP | `false` |
| `oidc.providerName` | Provider name shown in login button | `OAuth` |
| `oidc.rememberMe` | Extend session as if "Remember Me" was checked | `false` |
| `oidc.signingAlgorithm` | Algorithm used to sign the id token | `RS256` |
| `oidc.userClaim` | Claim to look up existing user by | `email` |
| `oidc.nameClaim` | Claim for user's full name | `name` |
| `oidc.groupsClaim` | Claim for user groups | `groups` |
### OpenAI Integration
| Name | Description | Value |
|------------------------------------|------------------------------------------|-----------|
| `openai.enabled` | Enable OpenAI integration | `false` |
| `openai.baseUrl` | Base URL for OpenAI API | `""` |
| `openai.apiKey` | OpenAI API key | `""` |
| `openai.model` | OpenAI model to use | `gpt-4o` |
| `openai.customHeaders` | Custom HTTP headers for OpenAI requests | `""` |
| `openai.customParams` | Custom HTTP query params for OpenAI requests | `""` |
| `openai.enableImageServices` | Enable OpenAI image services | `true` |
| `openai.workers` | Number of OpenAI workers per request | `2` |
| `openai.sendDatabaseData` | Send Mealie data to OpenAI to improve accuracy | `true` |
| `openai.requestTimeout` | Timeout for OpenAI requests in seconds | `60` |
### TLS Configuration
| Name | Description | Value |
|--------------------------|--------------------------------------|-----------|
| `tls.enabled` | Enable TLS configuration | `false` |
| `tls.certificatePath` | Path to TLS certificate file | `""` |
| `tls.privateKeyPath` | Path to TLS private key file | `""` |
| `tls.existingSecret` | Name of existing secret with TLS certificates | `""` |
| `tls.certificateKey` | Key in the secret for TLS certificate | `tls.crt` |
| `tls.privateKeyKey` | Key in the secret for TLS private key | `tls.key` |
### Theme Configuration
| Name | Description | Value |
|-------------------------------|--------------------------------|-----------|
| `theme.light.primary` | Light theme primary color | `#E58325` |
| `theme.light.accent` | Light theme accent color | `#007A99` |
| `theme.light.secondary` | Light theme secondary color | `#973542` |
| `theme.light.success` | Light theme success color | `#43A047` |
| `theme.light.info` | Light theme info color | `#1976D2` |
| `theme.light.warning` | Light theme warning color | `#FF6D00` |
| `theme.light.error` | Light theme error color | `#EF5350` |
| `theme.dark.primary` | Dark theme primary color | `#E58325` |
| `theme.dark.accent` | Dark theme accent color | `#007A99` |
| `theme.dark.secondary` | Dark theme secondary color | `#973542` |
| `theme.dark.success` | Dark theme success color | `#43A047` |
| `theme.dark.info` | Dark theme info color | `#1976D2` |
| `theme.dark.warning` | Dark theme warning color | `#FF6D00` |
| `theme.dark.error` | Dark theme error color | `#EF5350` |
### Resource Configuration
| Name | Description | Value |
|-------------|--------------------------------------|-------|
| `resources` | Resource limits and requests | `{}` |
### Health Checks
| Name | Description | Value |
|-------------------------------------------|------------------------------------------|-------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.initialDelaySeconds` | Initial delay for liveness probe | `60` |
| `probes.liveness.periodSeconds` | Period for liveness probe | `30` |
| `probes.liveness.timeoutSeconds` | Timeout for liveness probe | `10` |
| `probes.liveness.failureThreshold` | Failure threshold for liveness probe | `3` |
| `probes.liveness.successThreshold` | Success threshold for liveness probe | `1` |
| `probes.liveness.path` | Path for liveness probe | `/` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.initialDelaySeconds` | Initial delay for readiness probe | `30` |
| `probes.readiness.periodSeconds` | Period for readiness probe | `10` |
| `probes.readiness.timeoutSeconds` | Timeout for readiness probe | `5` |
| `probes.readiness.failureThreshold` | Failure threshold for readiness probe | `3` |
| `probes.readiness.successThreshold` | Success threshold for readiness probe | `1` |
| `probes.readiness.path` | Path for readiness probe | `/` |
### Autoscaling
| Name | Description | Value |
|---------------------------------------------|------------------------------------------|---------|
| `autoscaling.enabled` | Enable horizontal pod autoscaling | `false` |
| `autoscaling.minReplicas` | Minimum number of replicas | `1` |
| `autoscaling.maxReplicas` | Maximum number of replicas | `3` |
| `autoscaling.targetCPUUtilizationPercentage`| Target CPU utilization percentage | `80` |
| `autoscaling.targetMemoryUtilizationPercentage`| Target memory utilization percentage | `80` |
## Configuration Examples ## Configuration Examples
### Basic Installation with Persistence ### Minimal Installation
```yaml ```yaml
persistence: persistence:
enabled: true enabled: true
size: 10Gi size: 10Gi
storageClass: "fast-ssd"
ingress: ingress:
enabled: true enabled: true
@ -282,7 +50,7 @@ ingress:
secretName: mealie-tls secretName: mealie-tls
``` ```
### PostgreSQL Database Configuration ### PostgreSQL Configuration
```yaml ```yaml
postgresql: postgresql:
@ -300,7 +68,7 @@ env:
DB_ENGINE: "postgres" DB_ENGINE: "postgres"
``` ```
### OIDC Authentication Setup ### OIDC Authentication
```yaml ```yaml
oidc: oidc:
@ -326,27 +94,273 @@ openai:
enableImageServices: true enableImageServices: true
``` ```
## Security Considerations ### LDAP Authentication
For production deployments, it's recommended to: ```yaml
ldap:
enabled: true
serverUrl: "ldap://ldap.example.com"
baseDn: "ou=users,dc=example,dc=com"
queryBind: "cn=admin,dc=example,dc=com"
queryPassword: "bind-password"
userFilter: "(objectClass=inetOrgPerson)"
idAttribute: "uid"
nameAttribute: "name"
mailAttribute: "mail"
```
1. Use external secrets for sensitive information ### Email Configuration
2. Enable TLS/SSL for all communications
3. Configure proper RBAC and network policies ```yaml
4. Use a dedicated database with proper access controls email:
5. Enable authentication (LDAP/OIDC) and disable public signup enabled: true
host: "smtp.example.com"
port: 587
fromName: "Mealie"
fromEmail: "mealie@example.com"
authStrategy: "TLS"
existingSecret: "mealie-smtp-secret"
userKey: "smtp-user"
passwordKey: "smtp-password"
```
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Mealie image repository | `ghcr.io/mealie-recipes/mealie` |
| `image.tag` | Image tag | `v3.2.1` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `podSecurityContext.runAsNonRoot` | Run as non-root | `false` |
| `podSecurityContext.runAsUser` | User ID | `911` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `911` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `9000` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.enabled` | Enable persistence | `false` |
| `persistence.storageClass` | Storage class | `""` |
| `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | PVC size | `5Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Environment Variables
| Name | Description | Default |
|------|-------------|---------|
| `env.PUID` | User ID for host permissions | `911` |
| `env.PGID` | Group ID for host permissions | `911` |
| `env.DEFAULT_GROUP` | Default group for users | `Home` |
| `env.DEFAULT_HOUSEHOLD` | Default household | `Family` |
| `env.BASE_URL` | Base URL for notifications | `http://localhost:9000` |
| `env.TOKEN_TIME` | Login token validity (hours) | `48` |
| `env.API_PORT` | Backend API port | `9000` |
| `env.API_DOCS` | Enable API documentation | `true` |
| `env.TZ` | Timezone | `UTC` |
| `env.ALLOW_SIGNUP` | Allow user sign-up | `false` |
| `env.ALLOW_PASSWORD_LOGIN` | Allow password login | `true` |
| `env.LOG_LEVEL` | Log level | `info` |
| `env.DAILY_SCHEDULE_TIME` | Daily task schedule (HH:MM) | `23:45` |
| `env.DB_ENGINE` | Database engine (`postgres` or `sqlite`) | `postgres` |
| `extraEnv` | Additional environment variables | `[]` |
### PostgreSQL Configuration
| Name | Description | Default |
|------|-------------|---------|
| `postgresql.enabled` | Enable PostgreSQL | `false` |
| `postgresql.external.enabled` | Use external PostgreSQL | `false` |
| `postgresql.external.host` | PostgreSQL host | `""` |
| `postgresql.external.port` | PostgreSQL port | `5432` |
| `postgresql.external.database` | Database name | `mealie` |
| `postgresql.external.user` | Username | `mealie` |
| `postgresql.external.password` | Password | `""` |
| `postgresql.external.existingSecret` | Existing secret name | `""` |
| `postgresql.external.userKey` | Key for username in secret | `username` |
| `postgresql.external.passwordKey` | Key for password in secret | `password` |
### LDAP Authentication
| Name | Description | Default |
|------|-------------|---------|
| `ldap.enabled` | Enable LDAP | `false` |
| `ldap.serverUrl` | LDAP server URL | `""` |
| `ldap.tlsInsecure` | Skip server cert verification | `false` |
| `ldap.tlsCaCertFile` | CA certificate path | `""` |
| `ldap.enableStartTls` | Use STARTTLS | `false` |
| `ldap.baseDn` | Base DN for authentication | `""` |
| `ldap.queryBind` | Bind user for searches | `""` |
| `ldap.queryPassword` | Bind user password | `""` |
| `ldap.userFilter` | User LDAP filter | `""` |
| `ldap.adminFilter` | Admin LDAP filter | `""` |
| `ldap.idAttribute` | User ID attribute | `uid` |
| `ldap.nameAttribute` | User name attribute | `name` |
| `ldap.mailAttribute` | User email attribute | `mail` |
| `ldap.existingSecret` | Existing secret for LDAP | `""` |
| `ldap.passwordKey` | Key for password in secret | `ldap-password` |
### OIDC Authentication
| Name | Description | Default |
|------|-------------|---------|
| `oidc.enabled` | Enable OIDC | `false` |
| `oidc.signupEnabled` | Allow new users via OIDC | `true` |
| `oidc.configurationUrl` | OIDC configuration URL | `""` |
| `oidc.clientId` | Client ID | `""` |
| `oidc.clientSecret` | Client secret | `""` |
| `oidc.userGroup` | Required user group | `""` |
| `oidc.adminGroup` | Admin group | `""` |
| `oidc.autoRedirect` | Redirect to IdP on login | `false` |
| `oidc.providerName` | Provider name on login button | `OAuth` |
| `oidc.rememberMe` | Extend session ("Remember Me") | `false` |
| `oidc.signingAlgorithm` | ID token signing algorithm | `RS256` |
| `oidc.userClaim` | Claim to identify user | `email` |
| `oidc.nameClaim` | Claim for user name | `name` |
| `oidc.groupsClaim` | Claim for groups | `groups` |
| `oidc.existingSecret` | Existing secret name | `""` |
| `oidc.clientIdKey` | Key for client ID in secret | `oidc-client-id` |
| `oidc.clientSecretKey` | Key for client secret in secret | `oidc-client-secret` |
### OpenAI Configuration
| Name | Description | Default |
|------|-------------|---------|
| `openai.enabled` | Enable OpenAI | `false` |
| `openai.baseUrl` | OpenAI API base URL | `""` |
| `openai.apiKey` | OpenAI API key | `""` |
| `openai.model` | Model to use | `gpt-4o` |
| `openai.enableImageServices` | Enable image services | `true` |
| `openai.workers` | Workers per request | `2` |
| `openai.sendDatabaseData` | Send DB data for accuracy | `true` |
| `openai.requestTimeout` | Request timeout (seconds) | `60` |
| `openai.existingSecret` | Existing secret name | `""` |
| `openai.apiKeyKey` | Key for API key in secret | `openai-api-key` |
### Email Configuration
| Name | Description | Default |
|------|-------------|---------|
| `email.enabled` | Enable SMTP email | `false` |
| `email.host` | SMTP host | `""` |
| `email.port` | SMTP port | `587` |
| `email.fromName` | From name | `Mealie` |
| `email.authStrategy` | Auth strategy (`TLS`, `SSL`, `NONE`) | `TLS` |
| `email.fromEmail` | From email address | `""` |
| `email.user` | SMTP username | `""` |
| `email.password` | SMTP password | `""` |
| `email.existingSecret` | Existing secret for SMTP | `""` |
| `email.userKey` | Key for username in secret | `smtp-user` |
| `email.passwordKey` | Key for password in secret | `smtp-password` |
### TLS Configuration
| Name | Description | Default |
|------|-------------|---------|
| `tls.enabled` | Enable TLS | `false` |
| `tls.certificatePath` | TLS certificate path | `""` |
| `tls.privateKeyPath` | TLS private key path | `""` |
| `tls.existingSecret` | Existing secret with TLS certs | `""` |
| `tls.certificateKey` | Key for certificate in secret | `tls.crt` |
| `tls.privateKeyKey` | Key for private key in secret | `tls.key` |
### Theme Configuration
| Name | Description | Default |
|------|-------------|---------|
| `theme.light.primary` | Light theme primary color | `#E58325` |
| `theme.light.accent` | Light theme accent color | `#007A99` |
| `theme.light.secondary` | Light theme secondary color | `#973542` |
| `theme.light.success` | Light theme success color | `#43A047` |
| `theme.light.info` | Light theme info color | `#1976D2` |
| `theme.light.warning` | Light theme warning color | `#FF6D00` |
| `theme.light.error` | Light theme error color | `#EF5350` |
| `theme.dark.primary` | Dark theme primary color | `#E58325` |
| `theme.dark.accent` | Dark theme accent color | `#007A99` |
| `theme.dark.secondary` | Dark theme secondary color | `#973542` |
| `theme.dark.success` | Dark theme success color | `#43A047` |
| `theme.dark.info` | Dark theme info color | `#1976D2` |
| `theme.dark.warning` | Dark theme warning color | `#FF6D00` |
| `theme.dark.error` | Dark theme error color | `#EF5350` |
### Resource Parameters
| Name | Description | Default |
|------|-------------|---------|
| `resources` | Resource limits and requests | `{}` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Liveness probe path | `/` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `60` |
| `probes.liveness.periodSeconds` | Liveness period | `30` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `30` |
| `probes.readiness.periodSeconds` | Readiness period | `10` |
### Autoscaling Parameters
| Name | Description | Default |
|------|-------------|---------|
| `autoscaling.enabled` | Enable HPA | `false` |
| `autoscaling.minReplicas` | Min replicas | `1` |
| `autoscaling.maxReplicas` | Max replicas | `3` |
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU | `80` |
| `autoscaling.targetMemoryUtilizationPercentage` | Target memory | `80` |
## Troubleshooting ## Troubleshooting
Common issues and solutions: - **Database connection issues**: Verify credentials and network connectivity
- **Persistence issues**: Check StorageClass and PVC configuration
1. **Database connection issues**: Verify database credentials and network connectivity - **Authentication problems**: Verify LDAP/OIDC configuration and network access
2. **Persistence issues**: Check StorageClass and PVC configuration - **Performance issues**: Adjust resource limits and consider using an external database
3. **Authentication problems**: Verify LDAP/OIDC configuration and network access
4. **Performance issues**: Adjust resource limits and consider using external database
For more detailed troubleshooting, check the application logs:
```bash ```bash
kubectl logs -f deployment/mealie kubectl logs -f deployment/mealie
kubectl describe pod -l app.kubernetes.io/name=mealie
``` ```
## Links
- [Mealie GitHub](https://github.com/mealie-recipes/mealie)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/mealie)

View File

@ -2,8 +2,8 @@ apiVersion: v2
name: norish name: norish
description: Norish helm chart for Kubernetes - A recipe management and meal planning application description: Norish helm chart for Kubernetes - A recipe management and meal planning application
type: application type: application
version: 0.0.3 version: 0.0.5
appVersion: "v0.13.6-beta" appVersion: "v0.15.4-beta"
maintainers: maintainers:
- name: Richard Tomik - name: Richard Tomik
email: no@m.com email: no@m.com

View File

@ -1,86 +1,39 @@
cl# Norish Helm Chart # Norish Helm Chart
A Helm chart for deploying [Norish](https://github.com/norishapp/norish), a recipe management and meal planning application, on Kubernetes. A Helm chart for deploying [Norish](https://github.com/norishapp/norish), a recipe management and meal planning application, on Kubernetes.
## Introduction ## Introduction
This chart bootstraps a Norish deployment on a Kubernetes cluster using the Helm package manager. This chart deploys Norish on a Kubernetes cluster. Norish requires an external PostgreSQL database, a Redis server, and includes a Chrome headless sidecar for recipe parsing. It supports multiple authentication methods including password auth, OIDC, GitHub OAuth, and Google OAuth.
**IMPORTANT: This chart requires a central PostgreSQL database.** You must have a PostgreSQL server available before deploying this chart. The chart does not include a PostgreSQL deployment. Source code: https://github.com/rtomik/helm-charts/tree/main/charts/norish
**Note:** This chart includes a Chrome headless sidecar container that is required for recipe parsing and scraping functionality. Chrome requires elevated security privileges (`SYS_ADMIN` capability) and additional resources (recommend 256Mi-512Mi memory).
## Prerequisites ## Prerequisites
- Kubernetes 1.19+ - Kubernetes 1.19+
- Helm 3.0+ - Helm 3.0+
- **PostgreSQL database server** (required) - **PostgreSQL database** (required)
- PV provisioner support in the underlying infrastructure (if persistence is enabled) - **Redis server** (required)
- PV provisioner support (if persistence is enabled)
## Installing the Chart ## Installing the Chart
To install the chart with the release name `norish`:
```bash ```bash
$ helm repo add helm-charts https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
$ helm install norish helm-charts/norish helm install norish rtomik/norish
``` ```
The command deploys Norish on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation.
## Uninstalling the Chart ## Uninstalling the Chart
To uninstall/delete the `norish` deployment:
```bash ```bash
helm uninstall norish helm uninstall norish
``` ```
This command removes all the Kubernetes components associated with the chart and deletes the release. ## Configuration Examples
## Configuration ### Minimal Installation (Password Authentication)
### Required Configuration
Before deploying, you must configure:
1. **PostgreSQL Database** (REQUIRED): A central PostgreSQL database must be available
- Configure `database.host` to point to your PostgreSQL server
- Ensure the database exists before deployment
- Set appropriate credentials
2. **Master Key**: A 32-byte base64-encoded encryption key
```bash
# Generate a master key
openssl rand -base64 32
```
3. **Application URL**: Set `config.authUrl` to match your ingress hostname
### Authentication Configuration
**Authentication providers are now optional!** You can deploy Norish in two ways:
**Option 1: Password Authentication (Simple Setup)**
- No external authentication provider required
- Users can register and log in with email/password
- Perfect for self-hosted, single-tenant deployments
- Enabled automatically when no OAuth/OIDC provider is configured
**Option 2: OAuth/OIDC Provider (Enterprise Setup)**
- Configure ONE of the following:
- OIDC/OAuth2
- GitHub OAuth
- Google OAuth
- Recommended for multi-user environments
- Can be combined with password authentication via `config.passwordAuthEnabled`
### Example: Minimal Installation (Password Authentication)
This is the simplest setup using built-in password authentication:
```yaml ```yaml
# values.yaml
database: database:
host: "postgresql.default.svc.cluster.local" host: "postgresql.default.svc.cluster.local"
port: 5432 port: 5432
@ -88,11 +41,15 @@ database:
username: norish username: norish
password: "secure-password" password: "secure-password"
redis:
host: "redis.default.svc.cluster.local"
port: 6379
database: 0
config: config:
authUrl: "https://norish.example.com" authUrl: "https://norish.example.com"
masterKey: masterKey:
value: "<your-32-byte-base64-key>" value: "<your-32-byte-base64-key>" # Generate: openssl rand -base64 32
# passwordAuthEnabled defaults to true when no OAuth/OIDC is configured
ingress: ingress:
enabled: true enabled: true
@ -106,355 +63,53 @@ ingress:
- norish.example.com - norish.example.com
``` ```
Install with: ### Production with Existing Secrets
```bash
$ helm repo add helm-charts https://rtomik.github.io/helm-charts
$ helm install norish helm-charts/norish -f values.yaml
```
### Example: Installation with OIDC
For enterprise deployments with an external identity provider:
```yaml ```yaml
# values.yaml
database:
host: "postgresql.default.svc.cluster.local"
port: 5432
name: norish
username: norish
password: "secure-password"
config:
authUrl: "https://norish.example.com"
masterKey:
value: "<your-32-byte-base64-key>"
# Optional: Allow both OIDC and password authentication
passwordAuthEnabled: "true"
auth:
oidc:
enabled: true
name: "MyAuth"
issuer: "https://auth.example.com"
clientId: "<your-client-id>"
clientSecret: "<your-client-secret>"
ingress:
enabled: true
hosts:
- host: norish.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- norish.example.com
```
Install with:
```bash
$ helm repo add helm-charts https://rtomik.github.io/helm-charts
$ helm install norish helm-charts/norish -f values.yaml
```
### Example: Using Existing Secrets
For production deployments, store sensitive data in Kubernetes secrets:
```yaml
# values.yaml
database: database:
host: "postgresql.default.svc.cluster.local" host: "postgresql.default.svc.cluster.local"
existingSecret: "norish-db-secret" existingSecret: "norish-db-secret"
usernameKey: "username" usernameKey: "username"
passwordKey: "password" passwordKey: "password"
redis:
existingSecret: "norish-redis-secret"
urlKey: "redis-url"
config: config:
authUrl: "https://norish.example.com"
masterKey: masterKey:
existingSecret: "norish-master-key" existingSecret: "norish-master-key"
secretKey: "master-key" secretKey: "master-key"
auth:
oidc:
enabled: true
name: "MyAuth"
issuer: "https://auth.example.com"
existingSecret: "norish-oidc-secret"
clientIdKey: "client-id"
clientSecretKey: "client-secret"
``` ```
Create the secrets: Create the required secrets:
```bash ```bash
# Database credentials
kubectl create secret generic norish-db-secret \ kubectl create secret generic norish-db-secret \
--from-literal=username="norish" \ --from-literal=username="norish" \
--from-literal=password="secure-db-password" --from-literal=password="secure-db-password"
# Master encryption key kubectl create secret generic norish-redis-secret \
--from-literal=redis-url="redis://username:password@redis.default.svc.cluster.local:6379/0"
kubectl create secret generic norish-master-key \ kubectl create secret generic norish-master-key \
--from-literal=master-key="$(openssl rand -base64 32)" --from-literal=master-key="$(openssl rand -base64 32)"
# OIDC credentials
kubectl create secret generic norish-oidc-secret \
--from-literal=client-id="<your-client-id>" \
--from-literal=client-secret="<your-client-secret>"
``` ```
### Example: Using Existing PVC ### OIDC Authentication
If you want to use an existing PersistentVolumeClaim for uploads storage:
```yaml
# values.yaml
persistence:
enabled: true
existingClaim: "my-existing-pvc"
```
This is useful when:
- You want to reuse storage from a previous installation
- You have pre-provisioned PVCs with specific configurations
- You're managing PVCs separately from the Helm chart
### Optional Configuration
Version v0.13.6-beta introduces additional optional configuration options:
```yaml
config:
# Log level configuration
logLevel: "info" # Options: trace, debug, info, warn, error, fatal
# Additional trusted origins (useful when behind a proxy or using multiple domains)
trustedOrigins: "http://192.168.1.100:3000,https://norish.example.com"
# Enable/disable password authentication
# Defaults to true when no OAuth/OIDC is configured, false otherwise
# Set to "true" to enable password auth alongside OAuth/OIDC
passwordAuthEnabled: "true"
auth:
oidc:
enabled: true
name: "MyAuth"
issuer: "https://auth.example.com"
# Optional: Custom well-known configuration URL
# By default derived from issuer
wellKnown: "https://auth.example.com/.well-known/openid-configuration"
clientId: "<your-client-id>"
clientSecret: "<your-client-secret>"
```
### Customizing Chrome Headless Resources
Chrome headless is required but you can customize its resource limits:
```yaml
chrome:
enabled: true # Must be true for v0.13.6+
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
```
### Setting Up PostgreSQL Database
You need to create the database before deploying this chart:
```sql
-- Connect to your PostgreSQL server
CREATE DATABASE norish;
CREATE USER norish WITH ENCRYPTED PASSWORD 'secure-password';
GRANT ALL PRIVILEGES ON DATABASE norish TO norish;
```
Or if using a centralized PostgreSQL Helm chart or service, ensure the database is created and accessible from your Kubernetes cluster.
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the chart name | `""` |
| `fullnameOverride` | Override the full resource names | `""` |
### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Norish image repository | `norishapp/norish` |
| `image.tag` | Norish image tag | `v0.13.6-beta` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Image pull secrets | `[]` |
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Number of old ReplicaSets to retain | `3` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Kubernetes service type | `ClusterIP` |
| `service.port` | Service port | `3000` |
| `service.annotations` | Service annotations | `{}` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | `{"traefik.ingress.kubernetes.io/router.entrypoints": "websecure"}` |
| `ingress.hosts` | Ingress hosts configuration | See values.yaml |
| `ingress.tls` | Ingress TLS configuration | See values.yaml |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.enabled` | Enable persistent storage | `true` |
| `persistence.existingClaim` | Use an existing PVC instead of creating a new one | `""` |
| `persistence.storageClass` | Storage class name | `""` |
| `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | Storage size | `5Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Application Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.authUrl` | Application URL (required) | `"http://norish.domain.com"` |
| `config.masterKey.value` | Master encryption key | `""` |
| `config.masterKey.existingSecret` | Use existing secret for master key | `""` |
| `config.logLevel` | Log level: trace, debug, info, warn, error, fatal | `""` |
| `config.trustedOrigins` | Additional trusted origins (comma-separated) | `""` |
| `config.passwordAuthEnabled` | Enable/disable password authentication (defaults to true when no OAuth/OIDC configured) | `""` |
| `config.auth.oidc.enabled` | Enable OIDC authentication | `false` |
| `config.auth.oidc.name` | OIDC provider name | `"MyAuth"` |
| `config.auth.oidc.issuer` | OIDC issuer URL | `""` |
| `config.auth.oidc.wellKnown` | OIDC well-known configuration URL (optional) | `""` |
| `config.auth.oidc.clientId` | OIDC client ID | `""` |
| `config.auth.oidc.clientSecret` | OIDC client secret | `""` |
| `config.auth.github.enabled` | Enable GitHub OAuth | `false` |
| `config.auth.github.clientId` | GitHub client ID | `""` |
| `config.auth.github.clientSecret` | GitHub client secret | `""` |
| `config.auth.google.enabled` | Enable Google OAuth | `false` |
| `config.auth.google.clientId` | Google client ID | `""` |
| `config.auth.google.clientSecret` | Google client secret | `""` |
### Database Parameters (REQUIRED)
| Name | Description | Default |
|------|-------------|---------|
| `database.host` | PostgreSQL database host (required) | `""` |
| `database.port` | PostgreSQL database port | `5432` |
| `database.name` | PostgreSQL database name | `norish` |
| `database.username` | PostgreSQL username | `postgres` |
| `database.password` | PostgreSQL password | `""` |
| `database.existingSecret` | Use existing secret for database credentials | `""` |
| `database.usernameKey` | Key in secret for username | `"username"` |
| `database.passwordKey` | Key in secret for password | `"password"` |
| `database.databaseKey` | Key in secret for database name | `"database"` |
| `database.hostKey` | Key in secret for host | `"host"` |
### Chrome Headless Parameters (REQUIRED)
| Name | Description | Default |
|------|-------------|---------|
| `chrome.enabled` | Enable Chrome headless sidecar (required for v0.13.6+) | `true` |
| `chrome.image.repository` | Chrome headless image repository | `zenika/alpine-chrome` |
| `chrome.image.tag` | Chrome headless image tag | `latest` |
| `chrome.image.pullPolicy` | Chrome image pull policy | `IfNotPresent` |
| `chrome.port` | Chrome remote debugging port | `3000` |
| `chrome.resources` | Chrome container resource limits/requests | `{}` |
### Security Parameters
| Name | Description | Default |
|------|-------------|---------|
| `podSecurityContext.runAsNonRoot` | Run as non-root user | `true` |
| `podSecurityContext.runAsUser` | User ID to run as | `1000` |
| `podSecurityContext.fsGroup` | Group ID for filesystem | `1000` |
### Resource Parameters
| Name | Description | Default |
|------|-------------|---------|
| `resources` | CPU/Memory resource requests/limits | `{}` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.startup.enabled` | Enable startup probe | `true` |
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
## What's New in v0.13.6-beta
This version introduces several improvements and new features:
**UI/UX Improvements:**
- Ability to change prompts used in Settings → Admin
- Improved transcriber logic
- Double tapping/clicking planned recipes now opens the recipe page
- Small icon that opens the original recipe page
- Add recipes button now opens a dropdown instead of instantly redirecting to manual creation
**New Features:**
- Support for trusting additional origins using `TRUSTED_ORIGINS` environment variable (comma-separated)
- Customizable password authentication via `PASSWORD_AUTH_ENABLED` flag
- Configurable log level via `NEXT_PUBLIC_LOG_LEVEL`
**Bug Fixes:**
- User menu remaining open when clicking import
- Text truncation no longer uses the tailwind truncate class in the calendar
- Comma decimals being parsed as nothing (e.g., 2,5 ended up as 25)
- Unicode character handling
**Breaking Changes:**
- Chrome headless is now mandatory for improved parsing functionality
## Authentication Setup
Norish v0.13.6-beta and later support multiple authentication methods:
### Password Authentication (Default)
When no external authentication provider is configured, Norish automatically enables password-based authentication. Users can:
- Register new accounts with email and password
- Log in using their credentials
- Manage their account through the web interface
This is the simplest setup and perfect for:
- Self-hosted, single-user or family deployments
- Testing and development environments
- Scenarios where external OAuth providers are not needed
### External Authentication Providers (Optional)
For enterprise or multi-tenant deployments, you can configure external authentication providers. After configuring a provider, you can manage additional authentication methods through the Settings → Admin interface.
### OIDC/OAuth2
```yaml ```yaml
config: config:
auth: auth:
oidc: oidc:
enabled: true enabled: true
name: "Authentik" # Display name name: "Authentik"
issuer: "https://auth.example.com/application/o/norish/" issuer: "https://auth.example.com/application/o/norish/"
clientId: "<your-client-id>" clientId: "<your-client-id>"
clientSecret: "<your-client-secret>" clientSecret: "<your-client-secret>"
# Optional: allow password auth alongside OIDC
passwordAuthEnabled: "true"
``` ```
### GitHub OAuth ### GitHub OAuth
@ -485,60 +140,216 @@ config:
clientSecret: "<your-google-client-secret>" clientSecret: "<your-google-client-secret>"
``` ```
### Using Existing PVC
```yaml
persistence:
enabled: true
existingClaim: "my-existing-pvc"
```
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Norish image repository | `norishapp/norish` |
| `image.tag` | Image tag | `v0.15.4-beta` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Image pull secrets | `[]` |
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `podSecurityContext.runAsNonRoot` | Run as non-root | `true` |
| `podSecurityContext.runAsUser` | User ID | `1000` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `1000` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
| `podAnnotations` | Pod annotations | `{}` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `3000` |
| `service.annotations` | Service annotations | `{}` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.enabled` | Enable persistence | `true` |
| `persistence.existingClaim` | Use an existing PVC | `""` |
| `persistence.storageClass` | Storage class | `""` |
| `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | PVC size | `5Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Database Configuration (Required)
| Name | Description | Default |
|------|-------------|---------|
| `database.host` | PostgreSQL host | `""` |
| `database.port` | PostgreSQL port | `5432` |
| `database.name` | Database name | `norish` |
| `database.username` | Username | `postgres` |
| `database.password` | Password | `""` |
| `database.existingSecret` | Existing secret name | `""` |
| `database.usernameKey` | Key for username in secret | `username` |
| `database.passwordKey` | Key for password in secret | `password` |
| `database.databaseKey` | Key for database name in secret | `database` |
| `database.hostKey` | Key for host in secret | `""` |
### Redis Configuration (Required)
| Name | Description | Default |
|------|-------------|---------|
| `redis.host` | Redis host | `""` |
| `redis.port` | Redis port | `6379` |
| `redis.database` | Redis database number | `0` |
| `redis.username` | Redis username (6.0+) | `""` |
| `redis.password` | Redis password | `""` |
| `redis.existingSecret` | Existing secret name | `""` |
| `redis.urlKey` | Key for full Redis URL in secret | `redis-url` |
| `redis.passwordKey` | Key for password in secret | `password` |
### Application Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.authUrl` | Application URL (must match ingress) | `http://norish.domain.com` |
| `config.logLevel` | Log level (`trace`, `debug`, `info`, `warn`, `error`, `fatal`) | `""` |
| `config.trustedOrigins` | Additional trusted origins (comma-separated) | `""` |
| `config.passwordAuthEnabled` | Enable/disable password auth | `""` |
| `config.extraEnv` | Extra environment variables | `[]` |
### Master Key Configuration (Required)
| Name | Description | Default |
|------|-------------|---------|
| `config.masterKey.value` | 32-byte base64 encryption key | `""` |
| `config.masterKey.existingSecret` | Existing secret name | `""` |
| `config.masterKey.secretKey` | Key in secret | `master-key` |
Generate with: `openssl rand -base64 32`
### OIDC Authentication
| Name | Description | Default |
|------|-------------|---------|
| `config.auth.oidc.enabled` | Enable OIDC | `false` |
| `config.auth.oidc.name` | Provider display name | `MyAuth` |
| `config.auth.oidc.issuer` | OIDC issuer URL | `""` |
| `config.auth.oidc.clientId` | Client ID | `""` |
| `config.auth.oidc.clientSecret` | Client secret | `""` |
| `config.auth.oidc.wellKnown` | Well-known URL (optional) | `""` |
| `config.auth.oidc.existingSecret` | Existing secret name | `""` |
| `config.auth.oidc.clientIdKey` | Key for client ID in secret | `oidc-client-id` |
| `config.auth.oidc.clientSecretKey` | Key for client secret in secret | `oidc-client-secret` |
### GitHub OAuth
| Name | Description | Default |
|------|-------------|---------|
| `config.auth.github.enabled` | Enable GitHub OAuth | `false` |
| `config.auth.github.clientId` | Client ID | `""` |
| `config.auth.github.clientSecret` | Client secret | `""` |
| `config.auth.github.existingSecret` | Existing secret name | `""` |
| `config.auth.github.clientIdKey` | Key for client ID in secret | `github-client-id` |
| `config.auth.github.clientSecretKey` | Key for client secret in secret | `github-client-secret` |
### Google OAuth
| Name | Description | Default |
|------|-------------|---------|
| `config.auth.google.enabled` | Enable Google OAuth | `false` |
| `config.auth.google.clientId` | Client ID | `""` |
| `config.auth.google.clientSecret` | Client secret | `""` |
| `config.auth.google.existingSecret` | Existing secret name | `""` |
| `config.auth.google.clientIdKey` | Key for client ID in secret | `google-client-id` |
| `config.auth.google.clientSecretKey` | Key for client secret in secret | `google-client-secret` |
### Chrome Headless Parameters
| Name | Description | Default |
|------|-------------|---------|
| `chrome.enabled` | Enable Chrome sidecar | `true` |
| `chrome.image.repository` | Chrome image repository | `zenika/alpine-chrome` |
| `chrome.image.tag` | Chrome image tag | `latest` |
| `chrome.image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `chrome.port` | Chrome debugging port | `9222` |
| `chrome.securityContext` | Chrome security context (requires root + SYS_ADMIN) | See values.yaml |
| `chrome.resources` | Chrome resource limits | `{}` |
### Resource Parameters
| Name | Description | Default |
|------|-------------|---------|
| `resources` | Resource limits and requests | `{}` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.startup.enabled` | Enable startup probe | `true` |
| `probes.startup.initialDelaySeconds` | Startup initial delay | `10` |
| `probes.startup.periodSeconds` | Startup period | `10` |
| `probes.startup.failureThreshold` | Startup failure threshold | `30` |
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `30` |
| `probes.liveness.periodSeconds` | Liveness period | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `5` |
| `probes.readiness.periodSeconds` | Readiness period | `5` |
## Upgrading
### From v0.13.x to v0.14.x
**Breaking change**: Redis is now required. Configure Redis before upgrading (see [Redis Configuration](#redis-configuration-required)).
### From v0.14.x to v0.15.x
No configuration changes required. Redis, PostgreSQL, and Chrome headless are already configured. Back up your database before upgrading as a precaution.
## Troubleshooting ## Troubleshooting
### Check Pod Status - **Master Key Not Set**: Generate with `openssl rand -base64 32`
- **Login Failures**: Password auth is enabled by default when no OAuth/OIDC is configured. Verify callback URLs match your ingress hostname.
- **Database Connection Failed**: Verify host, credentials, and that the database exists.
- **Chrome Headless Issues**: Chrome requires `SYS_ADMIN` capability and 256Mi-512Mi memory. Check logs with `kubectl logs -l app.kubernetes.io/name=norish -c chrome-headless`
- **Recipe Parsing Failures**: Ensure Chrome is running. `CHROME_WS_ENDPOINT` is automatically configured by the chart.
```bash ```bash
kubectl get pods -l app.kubernetes.io/name=norish kubectl get pods -l app.kubernetes.io/name=norish
kubectl logs -l app.kubernetes.io/name=norish kubectl logs -l app.kubernetes.io/name=norish
``` ```
### Check Database Connection ## Links
```bash - [Norish GitHub](https://github.com/norishapp/norish)
# Test connection from app pod - [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/norish)
kubectl exec -it deployment/norish -- sh
nc -zv <your-postgres-host> 5432
```
### Common Issues
1. **Master Key Not Set**: Ensure you've generated and configured a master key
2. **Cannot Log In**:
- Password authentication is enabled by default when no OAuth/OIDC is configured
- If you configured an external provider, ensure the client ID/secret are correct
- Check the callback URL matches your ingress hostname
3. **Database Connection Failed**:
- Verify database host is correct and accessible from the cluster
- Check database credentials
- Ensure the database exists
- Verify network policies allow connections to the database
4. **Application Not Accessible**: Verify ingress configuration and DNS records
5. **Chrome Headless Issues**:
- Chrome requires `SYS_ADMIN` capability for proper operation
- If pod fails to start, check if your cluster's security policies allow the required capabilities
- Chrome container may require additional memory (256Mi-512Mi recommended)
- Check Chrome container logs: `kubectl logs -l app.kubernetes.io/name=norish -c chrome-headless`
6. **Recipe Parsing Failures**:
- Ensure Chrome headless is running: `kubectl get pods -l app.kubernetes.io/name=norish`
- Verify `CHROME_WS_ENDPOINT` is set correctly (automatically configured by the chart)
- Check if Chrome is accessible from the Norish container
## Upgrading
To upgrade the chart:
```bash
$ helm upgrade norish helm-charts/norish -f values.yaml
```
## Support
- Norish Repository: https://github.com/norishapp/norish
- Chart Repository: https://github.com/rtomik/helm-charts
- Issue Tracker: https://github.com/rtomik/helm-charts/issues
## License
This Helm chart is provided as-is under the same license as the Norish application.

View File

@ -55,3 +55,43 @@ Database connection URL
{{- $database := .Values.database.name }} {{- $database := .Values.database.name }}
{{- printf "postgres://%s:%s@%s:%d/%s" $username $password $host (int $port) $database }} {{- printf "postgres://%s:%s@%s:%d/%s" $username $password $host (int $port) $database }}
{{- end }} {{- end }}
{{/*
Redis URL (for non-authenticated Redis)
Constructs the Redis URL without authentication.
Format: redis://host:port/database
*/}}
{{- define "norish.redis.url.noauth" -}}
{{- $host := .Values.redis.host }}
{{- $port := .Values.redis.port }}
{{- $database := .Values.redis.database | toString }}
{{- printf "redis://%s:%d/%s" $host (int $port) $database }}
{{- end }}
{{/*
Check if Redis authentication is configured
Returns true if either existingSecret or password is set
*/}}
{{- define "norish.redis.hasAuth" -}}
{{- if or .Values.redis.existingSecret .Values.redis.password }}
{{- "true" }}
{{- end }}
{{- end }}
{{/*
Redis URL with authentication (for secret generation)
Constructs the Redis URL with password interpolation for use in secrets.
Format: redis://[username]:[password]@host:port/database
*/}}
{{- define "norish.redis.url.withPassword" -}}
{{- $host := .Values.redis.host }}
{{- $port := .Values.redis.port }}
{{- $database := .Values.redis.database | toString }}
{{- $username := .Values.redis.username | default "" }}
{{- $password := .Values.redis.password | default "" }}
{{- if $username }}
{{- printf "redis://%s:%s@%s:%d/%s" $username $password $host (int $port) $database }}
{{- else }}
{{- printf "redis://:%s@%s:%d/%s" $password $host (int $port) $database }}
{{- end }}
{{- end }}

View File

@ -145,6 +145,16 @@ spec:
name: {{ include "norish.fullname" . }}-secret name: {{ include "norish.fullname" . }}-secret
key: master-key key: master-key
{{- end }} {{- end }}
- name: REDIS_URL
valueFrom:
secretKeyRef:
{{- if .Values.redis.existingSecret }}
name: {{ .Values.redis.existingSecret }}
key: {{ .Values.redis.urlKey | default "redis-url" }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: redis-url
{{- end }}
{{- if .Values.config.auth.oidc.enabled }} {{- if .Values.config.auth.oidc.enabled }}
- name: OIDC_NAME - name: OIDC_NAME
value: {{ .Values.config.auth.oidc.name | quote }} value: {{ .Values.config.auth.oidc.name | quote }}

View File

@ -12,6 +12,13 @@ stringData:
{{- if not .Values.database.existingSecret }} {{- if not .Values.database.existingSecret }}
database-url: {{ include "norish.databaseUrl" . | quote }} database-url: {{ include "norish.databaseUrl" . | quote }}
{{- end }} {{- end }}
{{- if not .Values.redis.existingSecret }}
{{- if .Values.redis.password }}
redis-url: {{ include "norish.redis.url.withPassword" . | quote }}
{{- else }}
redis-url: {{ include "norish.redis.url.noauth" . | quote }}
{{- end }}
{{- end }}
{{- if .Values.config.auth.oidc.enabled }} {{- if .Values.config.auth.oidc.enabled }}
{{- if not .Values.config.auth.oidc.existingSecret }} {{- if not .Values.config.auth.oidc.existingSecret }}
oidc-client-id: {{ .Values.config.auth.oidc.clientId | quote }} oidc-client-id: {{ .Values.config.auth.oidc.clientId | quote }}

View File

@ -5,7 +5,7 @@ fullnameOverride: ""
## Image settings ## Image settings
image: image:
repository: norishapp/norish repository: norishapp/norish
tag: "v0.13.6-beta" tag: "v0.16.2-beta"
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
imagePullSecrets: [] imagePullSecrets: []
@ -211,6 +211,24 @@ database:
databaseKey: "database" # Key in the secret for database name (optional) databaseKey: "database" # Key in the secret for database name (optional)
hostKey: "" # Key in the secret for database host (optional) hostKey: "" # Key in the secret for database host (optional)
## External Redis configuration (REQUIRED for v0.14.0+)
## Redis is required for job queues and background tasks starting from v0.14.0-beta
redis:
# Redis connection details
host: "" # Required: Redis server hostname
port: 6379
database: 0
# Authentication (leave empty if Redis has no auth)
username: "" # Optional: Redis username (Redis 6.0+)
password: "" # Redis password (leave empty if no auth)
# Use existing secret for Redis credentials (recommended for production)
# NOTE: When using existingSecret, the secret MUST contain a key with the full Redis URL
# Format: redis://[username]:[password]@host:port/database
existingSecret: "" # Name of existing Kubernetes secret
urlKey: "redis-url" # Key in existingSecret containing the full Redis URL
passwordKey: "password" # Key in existingSecret for password (for compatibility)
## Chrome Headless configuration (REQUIRED) ## Chrome Headless configuration (REQUIRED)
## Required for improved recipe parsing and scraping ## Required for improved recipe parsing and scraping
chrome: chrome:

View File

@ -2,11 +2,11 @@ apiVersion: v2
name: paperless-ngx name: paperless-ngx
description: Paperless-ngx helm chart for Kubernetes description: Paperless-ngx helm chart for Kubernetes
type: application type: application
version: 0.0.2 version: 0.0.5
appVersion: "latest" appVersion: "2.20.3"
maintainers: maintainers:
- name: Richard Tomik - name: Richard Tomik
email: no@m.com email: richard.tomik@proton.me
keywords: keywords:
- productivity - productivity
- document-management - document-management

View File

@ -1,172 +1,59 @@
# Paperless-ngx Helm Chart # Paperless-ngx Helm Chart
A Helm chart for deploying Paperless-ngx document management system on Kubernetes. A Helm chart for deploying [Paperless-ngx](https://github.com/paperless-ngx/paperless-ngx), a document management system with OCR, on Kubernetes.
## Introduction ## Introduction
This chart deploys [Paperless-ngx](https://github.com/paperless-ngx/paperless-ngx) on a Kubernetes cluster using the Helm package manager. This chart deploys Paperless-ngx on a Kubernetes cluster. Paperless-ngx is a community-supported document scanner: scan, index, and archive all your physical documents. It requires external PostgreSQL and Redis services.
Paperless-ngx is a community-supported supercharged version of paperless: scan, index and archive all your physical documents. Source code: https://github.com/rtomik/helm-charts/tree/main/charts/paperless-ngx
Source code can be found here:
- https://github.com/rtomik/helm-charts/tree/main/charts/paperless-ngx
## Prerequisites ## Prerequisites
- Kubernetes 1.19+ - Kubernetes 1.19+
- Helm 3.0+ - Helm 3.0+
- PV provisioner support in the underlying infrastructure - **External PostgreSQL database** (PostgreSQL 11+ required)
- **External PostgreSQL database** (required) - **External Redis server**
- **External Redis server** (required) - PV provisioner support
## External Dependencies
This chart requires external PostgreSQL and Redis services. It does not deploy these dependencies to avoid resource conflicts on centralized servers.
### PostgreSQL Setup
Paperless-ngx requires PostgreSQL 11+ as its database backend. Ensure you have:
- A PostgreSQL database created for Paperless-ngx
- Database credentials configured in values.yaml or via secrets
### Redis Setup
Redis is required for background task processing. Ensure you have:
- A Redis server accessible from the cluster
- Connection details configured in values.yaml
- Optional: Redis authentication credentials (username/password)
- Optional: Redis key prefix for sharing one Redis server among multiple Paperless instances
The chart supports all Redis authentication methods:
- No authentication: `redis://host:port/database`
- Password only (requirepass): `redis://:password@host:port/database`
- Username and password (Redis 6.0+ ACL): `redis://username:password@host:port/database`
## Installing the Chart ## Installing the Chart
To install the chart with the release name `paperless-ngx`:
```bash ```bash
$ helm repo add paperless-chart https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
$ helm install paperless-ngx paperless-chart/paperless-ngx helm install paperless-ngx rtomik/paperless-ngx
``` ```
Or install directly from this repository: ## Uninstalling the Chart
```bash ```bash
$ git clone https://github.com/rtomik/helm-charts.git helm uninstall paperless-ngx
$ cd helm-charts/charts/paperless-ngx
$ helm install paperless-ngx .
``` ```
> **Tip**: List all releases using `helm list` **Note**: PVCs are not deleted automatically. To remove them:
## Configuration
The following table lists the configurable parameters and their default values.
### Global Parameters
| Name | Description | Value |
|------------------------|-------------------------------------------------------------------------------------|-------|
| `nameOverride` | String to partially override the release name | `""` |
| `fullnameOverride` | String to fully override the release name | `""` |
### Image Parameters
| Name | Description | Value |
|-------------------------|--------------------------------------------------------------------------------------|--------------------|
| `image.repository` | Paperless-ngx image repository | `ghcr.io/paperless-ngx/paperless-ngx` |
| `image.tag` | Paperless-ngx image tag | `latest` |
| `image.pullPolicy` | Paperless-ngx image pull policy | `IfNotPresent` |
### External Dependencies
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|-------------------------------------------|
| `postgresql.external.enabled` | Enable external PostgreSQL configuration | `true` |
| `postgresql.external.host` | External PostgreSQL host | `postgresql.default.svc.cluster.local` |
| `postgresql.external.port` | External PostgreSQL port | `5432` |
| `postgresql.external.database` | External PostgreSQL database name | `paperless` |
| `postgresql.external.username` | External PostgreSQL username | `paperless` |
| `postgresql.external.existingSecret` | Existing secret with PostgreSQL credentials | `""` |
| `postgresql.external.passwordKey` | Key in existing secret for PostgreSQL password | `postgresql-password` |
| `redis.external.enabled` | Enable external Redis configuration | `true` |
| `redis.external.host` | External Redis host | `redis.default.svc.cluster.local` |
| `redis.external.port` | External Redis port | `6379` |
| `redis.external.database` | External Redis database number | `0` |
| `redis.external.username` | Redis username (Redis 6.0+ with ACL) | `""` |
| `redis.external.password` | Redis password (leave empty if no auth required) | `""` |
| `redis.external.existingSecret` | Existing secret with Redis credentials | `""` |
| `redis.external.passwordKey` | Key in existing secret for Redis password | `redis-password` |
| `redis.external.prefix` | Prefix for Redis keys/channels (for multi-instance) | `""` |
### Security Configuration
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|---------------------|
| `config.secretKey.existingSecret` | Name of existing secret for Django secret key | `""` |
| `config.secretKey.secretKey` | Key in the existing secret for Django secret key | `secret-key` |
| `config.admin.user` | Admin username to create on startup | `""` |
| `config.admin.password` | Admin password (use existingSecret for production) | `""` |
| `config.admin.email` | Admin email address | `root@localhost` |
| `config.admin.existingSecret` | Name of existing secret for admin credentials | `""` |
### Application Configuration
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|---------------------|
| `config.url` | External URL for Paperless-ngx (e.g., https://paperless.domain.com) | `""` |
| `config.allowedHosts` | Comma-separated list of allowed hosts | `*` |
| `config.timeZone` | Application timezone | `UTC` |
| `config.ocr.language` | OCR language (3-letter code) | `eng` |
| `config.ocr.mode` | OCR mode (skip, redo, force) | `skip` |
| `config.consumer.recursive` | Enable recursive consumption directory watching | `false` |
| `config.consumer.subdirsAsTags` | Use subdirectory names as tags | `false` |
### Persistence Parameters
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|---------------------|
| `persistence.data.enabled` | Enable persistence for data directory | `true` |
| `persistence.data.size` | Size of data PVC | `1Gi` |
| `persistence.media.enabled` | Enable persistence for media directory | `true` |
| `persistence.media.size` | Size of media PVC | `10Gi` |
| `persistence.consume.enabled` | Enable persistence for consume directory | `true` |
| `persistence.consume.size` | Size of consume PVC | `5Gi` |
| `persistence.export.enabled` | Enable persistence for export directory | `true` |
| `persistence.export.size` | Size of export PVC | `1Gi` |
### Service Parameters
| Name | Description | Value |
|----------------------------|------------------------------------------------------|-------------|
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `8000` |
### Ingress Parameters
| Name | Description | Value |
|----------------------------|------------------------------------------------------|----------------------|
| `ingress.enabled` | Enable ingress record generation | `false` |
| `ingress.className` | IngressClass name | `""` |
| `ingress.annotations` | Additional annotations for the Ingress resource | See values.yaml |
| `ingress.hosts` | Array of host and path objects | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
## Usage Examples
### Basic Installation
```bash ```bash
helm install paperless-ngx . \ kubectl delete pvc -l app.kubernetes.io/instance=paperless-ngx
--set postgresql.external.host=my-postgres.example.com \
--set postgresql.external.password=secretpassword \
--set redis.external.host=my-redis.example.com
``` ```
### Production Installation with External Secrets ## Configuration Examples
### Minimal Installation
```yaml
postgresql:
external:
enabled: true
host: "my-postgres.example.com"
password: "secretpassword"
redis:
external:
host: "my-redis.example.com"
```
### Production with Existing Secrets
```yaml ```yaml
# values-production.yaml
config: config:
url: "https://paperless.example.com" url: "https://paperless.example.com"
allowedHosts: "paperless.example.com" allowedHosts: "paperless.example.com"
@ -178,14 +65,12 @@ config:
existingSecret: "paperless-admin-secrets" existingSecret: "paperless-admin-secrets"
postgresql: postgresql:
# External PostgreSQL connection details
external: external:
enabled: true enabled: true
host: "postgres-cluster-pooler.dbs.svc.cluster.local" host: "postgres-cluster-pooler.dbs.svc.cluster.local"
port: 5432 port: 5432
database: "paperless" database: "paperless"
username: "paperless" username: "paperless"
# Use existingSecret for credentials
existingSecret: "paperless-db-credentials" existingSecret: "paperless-db-credentials"
passwordKey: "password" passwordKey: "password"
@ -194,10 +79,8 @@ redis:
host: "redis.cache.svc.cluster.local" host: "redis.cache.svc.cluster.local"
port: 6379 port: 6379
database: 0 database: 0
# Use existingSecret for Redis credentials
existingSecret: "paperless-redis-credentials" existingSecret: "paperless-redis-credentials"
passwordKey: "password" passwordKey: "password"
# Optional: Use prefix to share Redis among multiple instances
prefix: "paperless-prod" prefix: "paperless-prod"
ingress: ingress:
@ -214,40 +97,17 @@ ingress:
- paperless.example.com - paperless.example.com
``` ```
```bash ### Redis with Username and Password (ACL)
helm install paperless-ngx . -f values-production.yaml
```
### Redis Authentication Examples
#### Redis with Password Only (requirepass)
```bash
helm install paperless-ngx . \
--set redis.external.host=redis.example.com \
--set redis.external.password=myredispassword
```
Or with existing secret:
```yaml ```yaml
redis: redis:
external: external:
host: "redis.example.com" host: "redis.example.com"
existingSecret: "redis-auth-secret" username: "paperless-user"
passwordKey: "redis-password" password: "myredispassword"
``` ```
#### Redis with Username and Password (Redis 6.0+ ACL) ### Sharing Redis Among Multiple Instances
```bash
helm install paperless-ngx . \
--set redis.external.host=redis.example.com \
--set redis.external.username=paperless-user \
--set redis.external.password=myredispassword
```
#### Multiple Paperless Instances on One Redis Server
Use the `prefix` parameter to avoid key collisions: Use the `prefix` parameter to avoid key collisions:
@ -267,47 +127,183 @@ redis:
prefix: "paperless-staging" prefix: "paperless-staging"
``` ```
## Security Considerations ### Using Existing PVCs
1. **Use external secrets** for production deployments to store sensitive data like database passwords, Redis passwords, and the Django secret key. ```yaml
2. **Set a proper PAPERLESS_URL** when exposing the application externally. persistence:
3. **Configure ALLOWED_HOSTS** to restrict which hosts can access the application. data:
4. **Use HTTPS** when exposing the application to the internet. enabled: true
5. **Secure Redis**: Always use authentication (password or username/password) for Redis in production environments. Use `existingSecret` instead of plain text passwords. existingClaim: "my-existing-data-pvc"
6. **Container Security**: The container runs as root initially to allow s6-overlay to set up the runtime environment, then drops privileges to UID 1000. This is required for the Paperless-ngx Docker image to function properly. media:
enabled: true
## Volumes and Data existingClaim: "my-existing-media-pvc"
export:
Paperless-ngx uses several directories: enabled: true
consume:
- **Data directory**: Contains the search index, classification model, and SQLite database (if used) enabled: true
- **Media directory**: Contains all uploaded documents and thumbnails
- **Consume directory**: Drop documents here for automatic processing
- **Export directory**: Used for document exports
All directories can be configured with separate PVCs and storage classes.
## Uninstalling the Chart
To uninstall/delete the `paperless-ngx` deployment:
```bash
helm uninstall paperless-ngx
``` ```
The command removes all the Kubernetes components associated with the chart and deletes the release. When `existingClaim` is set, the chart skips PVC creation and `storageClass`/`size` are ignored for that volume.
## Contributing ## Parameters
Please feel free to contribute by opening issues or pull requests at: ### Global Parameters
https://github.com/rtomik/helm-charts
## License | Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
This Helm chart is licensed under the MIT License. ### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Paperless-ngx image repository | `ghcr.io/paperless-ngx/paperless-ngx` |
| `image.tag` | Image tag | `2.20.3` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `podSecurityContext.runAsNonRoot` | Run as non-root | `false` |
| `podSecurityContext.runAsUser` | User ID | `0` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `1000` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `8000` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### PostgreSQL Configuration (Required)
| Name | Description | Default |
|------|-------------|---------|
| `postgresql.external.enabled` | Enable external PostgreSQL | `true` |
| `postgresql.external.host` | PostgreSQL host | `postgresql.default.svc.cluster.local` |
| `postgresql.external.port` | PostgreSQL port | `5432` |
| `postgresql.external.database` | Database name | `paperless` |
| `postgresql.external.username` | Username | `paperless` |
| `postgresql.external.password` | Password | `""` |
| `postgresql.external.existingSecret` | Existing secret name | `""` |
| `postgresql.external.passwordKey` | Key for password in secret | `postgresql-password` |
### Redis Configuration (Required)
| Name | Description | Default |
|------|-------------|---------|
| `redis.external.enabled` | Enable external Redis | `true` |
| `redis.external.host` | Redis host | `redis.default.svc.cluster.local` |
| `redis.external.port` | Redis port | `6379` |
| `redis.external.database` | Redis database number | `0` |
| `redis.external.username` | Redis username (6.0+ ACL) | `""` |
| `redis.external.password` | Redis password | `""` |
| `redis.external.existingSecret` | Existing secret name | `""` |
| `redis.external.urlKey` | Key for full Redis URL in secret | `redis-url` |
| `redis.external.passwordKey` | Key for password in secret | `redis-password` |
| `redis.external.prefix` | Key prefix for multi-instance | `""` |
### Application Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.url` | External URL | `""` |
| `config.allowedHosts` | Allowed hosts (comma-separated) | `*` |
| `config.csrfTrustedOrigins` | CSRF trusted origins | `""` |
| `config.timeZone` | Timezone | `UTC` |
| `config.ocr.language` | OCR language (3-letter code) | `eng` |
| `config.ocr.mode` | OCR mode (`skip`, `redo`, `force`) | `skip` |
| `config.consumer.recursive` | Recursive consume directory | `false` |
| `config.consumer.subdirsAsTags` | Use subdirectory names as tags | `false` |
### Security Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.secretKey.existingSecret` | Existing secret for Django secret key | `""` |
| `config.secretKey.secretKey` | Key in secret | `secret-key` |
| `config.admin.user` | Admin username to create on startup | `""` |
| `config.admin.password` | Admin password | `""` |
| `config.admin.email` | Admin email | `root@localhost` |
| `config.admin.existingSecret` | Existing secret for admin credentials | `""` |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.data.enabled` | Enable data PVC | `true` |
| `persistence.data.existingClaim` | Existing data PVC | `""` |
| `persistence.data.size` | Data PVC size | `1Gi` |
| `persistence.media.enabled` | Enable media PVC | `true` |
| `persistence.media.existingClaim` | Existing media PVC | `""` |
| `persistence.media.size` | Media PVC size | `10Gi` |
| `persistence.consume.enabled` | Enable consume PVC | `true` |
| `persistence.consume.existingClaim` | Existing consume PVC | `""` |
| `persistence.consume.size` | Consume PVC size | `5Gi` |
| `persistence.export.enabled` | Enable export PVC | `true` |
| `persistence.export.existingClaim` | Existing export PVC | `""` |
| `persistence.export.size` | Export PVC size | `1Gi` |
### Resource Parameters
| Name | Description | Default |
|------|-------------|---------|
| `resources` | Resource limits and requests | `{}` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Liveness probe path | `/` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `60` |
| `probes.liveness.periodSeconds` | Liveness period | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `30` |
| `probes.readiness.periodSeconds` | Readiness period | `5` |
### Autoscaling Parameters
| Name | Description | Default |
|------|-------------|---------|
| `autoscaling.enabled` | Enable HPA | `false` |
| `autoscaling.minReplicas` | Min replicas | `1` |
| `autoscaling.maxReplicas` | Max replicas | `3` |
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU | `80` |
| `autoscaling.targetMemoryUtilizationPercentage` | Target memory | `80` |
## Troubleshooting
- **Database Connection**: Verify PostgreSQL credentials and that the database exists
- **Redis Connection**: Ensure Redis is accessible; use `prefix` if sharing Redis between instances
- **Allowed Hosts Error**: Set `config.allowedHosts` to your domain when exposed externally
- **Container Security**: The container runs as root initially for s6-overlay setup, then drops to UID 1000. This is required by the Paperless-ngx image.
```bash
kubectl logs -f deployment/paperless-ngx
kubectl describe pod -l app.kubernetes.io/name=paperless-ngx
```
## Links ## Links
- [Paperless-ngx GitHub](https://github.com/paperless-ngx/paperless-ngx)
- [Paperless-ngx Documentation](https://docs.paperless-ngx.com/) - [Paperless-ngx Documentation](https://docs.paperless-ngx.com/)
- [Paperless-ngx GitHub Repository](https://github.com/paperless-ngx/paperless-ngx) - [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/paperless-ngx)
- [Docker Hub](https://hub.docker.com/r/ghcr.io/paperless-ngx/paperless-ngx)

View File

@ -89,21 +89,42 @@ Redis port
{{- end }} {{- end }}
{{/* {{/*
Redis URL Redis URL (for non-authenticated Redis)
Constructs the Redis URL with optional authentication. Constructs the Redis URL without authentication.
Format: redis://host:port/database
*/}}
{{- define "paperless-ngx.redis.url.noauth" -}}
{{- $host := include "paperless-ngx.redis.host" . }}
{{- $port := include "paperless-ngx.redis.port" . }}
{{- $database := .Values.redis.external.database | toString }}
{{- printf "redis://%s:%s/%s" $host $port $database }}
{{- end }}
{{/*
Check if Redis authentication is configured
Returns true if either existingSecret or password is set
*/}}
{{- define "paperless-ngx.redis.hasAuth" -}}
{{- if or .Values.redis.external.existingSecret .Values.redis.external.password }}
{{- "true" }}
{{- end }}
{{- end }}
{{/*
Redis URL with authentication (for secret generation)
Constructs the Redis URL with password interpolation for use in secrets.
This uses the actual password value when building the secret.
Format: redis://[username]:[password]@host:port/database Format: redis://[username]:[password]@host:port/database
*/}} */}}
{{- define "paperless-ngx.redis.url" -}} {{- define "paperless-ngx.redis.url.withPassword" -}}
{{- $host := include "paperless-ngx.redis.host" . }} {{- $host := include "paperless-ngx.redis.host" . }}
{{- $port := include "paperless-ngx.redis.port" . }} {{- $port := include "paperless-ngx.redis.port" . }}
{{- $database := .Values.redis.external.database | toString }} {{- $database := .Values.redis.external.database | toString }}
{{- $username := .Values.redis.external.username | default "" }} {{- $username := .Values.redis.external.username | default "" }}
{{- $password := .Values.redis.external.password | default "" }} {{- $password := .Values.redis.external.password | default "" }}
{{- if and $username $password }} {{- if $username }}
{{- printf "redis://%s:%s@%s:%s/%s" $username $password $host $port $database }} {{- printf "redis://%s:%s@%s:%s/%s" $username $password $host $port $database }}
{{- else if $password }}
{{- printf "redis://:%s@%s:%s/%s" $password $host $port $database }}
{{- else }} {{- else }}
{{- printf "redis://%s:%s/%s" $host $port $database }} {{- printf "redis://:%s@%s:%s/%s" $password $host $port $database }}
{{- end }} {{- end }}
{{- end }} {{- end }}

View File

@ -67,12 +67,23 @@ spec:
{{- end }} {{- end }}
env: env:
# Required services # Required services
{{- if include "paperless-ngx.redis.hasAuth" . }}
# When Redis has authentication, read the full URL from secret
- name: PAPERLESS_REDIS - name: PAPERLESS_REDIS
value: {{ include "paperless-ngx.redis.url" . | quote }} valueFrom:
secretKeyRef:
name: {{ .Values.redis.external.existingSecret | default (printf "%s-secrets" (include "paperless-ngx.fullname" .)) }}
key: {{ .Values.redis.external.urlKey | default "redis-url" }}
{{- else }}
# When Redis has no authentication, use the simple URL
- name: PAPERLESS_REDIS
value: {{ include "paperless-ngx.redis.url.noauth" . | quote }}
{{- end }}
{{- if .Values.redis.external.prefix }} {{- if .Values.redis.external.prefix }}
- name: PAPERLESS_REDIS_PREFIX - name: PAPERLESS_REDIS_PREFIX
value: {{ .Values.redis.external.prefix | quote }} value: {{ .Values.redis.external.prefix | quote }}
{{- end }} {{- end }}
- name: PAPERLESS_DBHOST - name: PAPERLESS_DBHOST
value: {{ include "paperless-ngx.postgresql.host" . | quote }} value: {{ include "paperless-ngx.postgresql.host" . | quote }}
- name: PAPERLESS_DBPORT - name: PAPERLESS_DBPORT
@ -324,7 +335,7 @@ spec:
{{- if .Values.persistence.data.enabled }} {{- if .Values.persistence.data.enabled }}
- name: data - name: data
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ include "paperless-ngx.fullname" . }}-data claimName: {{ if .Values.persistence.data.existingClaim }}{{ .Values.persistence.data.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-data{{ end }}
{{- else }} {{- else }}
- name: data - name: data
emptyDir: {} emptyDir: {}
@ -332,7 +343,7 @@ spec:
{{- if .Values.persistence.media.enabled }} {{- if .Values.persistence.media.enabled }}
- name: media - name: media
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ include "paperless-ngx.fullname" . }}-media claimName: {{ if .Values.persistence.media.existingClaim }}{{ .Values.persistence.media.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-media{{ end }}
{{- else }} {{- else }}
- name: media - name: media
emptyDir: {} emptyDir: {}
@ -340,7 +351,7 @@ spec:
{{- if .Values.persistence.export.enabled }} {{- if .Values.persistence.export.enabled }}
- name: export - name: export
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ include "paperless-ngx.fullname" . }}-export claimName: {{ if .Values.persistence.export.existingClaim }}{{ .Values.persistence.export.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-export{{ end }}
{{- else }} {{- else }}
- name: export - name: export
emptyDir: {} emptyDir: {}
@ -348,7 +359,7 @@ spec:
{{- if .Values.persistence.consume.enabled }} {{- if .Values.persistence.consume.enabled }}
- name: consume - name: consume
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ include "paperless-ngx.fullname" . }}-consume claimName: {{ if .Values.persistence.consume.existingClaim }}{{ .Values.persistence.consume.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-consume{{ end }}
{{- else }} {{- else }}
- name: consume - name: consume
emptyDir: {} emptyDir: {}

View File

@ -1,4 +1,4 @@
{{- if .Values.persistence.data.enabled }} {{- if and .Values.persistence.data.enabled (not .Values.persistence.data.existingClaim) }}
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
@ -21,7 +21,7 @@ spec:
--- ---
{{- end }} {{- end }}
{{- if .Values.persistence.media.enabled }} {{- if and .Values.persistence.media.enabled (not .Values.persistence.media.existingClaim) }}
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
@ -44,7 +44,7 @@ spec:
--- ---
{{- end }} {{- end }}
{{- if .Values.persistence.export.enabled }} {{- if and .Values.persistence.export.enabled (not .Values.persistence.export.existingClaim) }}
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
@ -67,7 +67,7 @@ spec:
--- ---
{{- end }} {{- end }}
{{- if .Values.persistence.consume.enabled }} {{- if and .Values.persistence.consume.enabled (not .Values.persistence.consume.existingClaim) }}
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:

View File

@ -32,6 +32,7 @@ data:
{{- end }} {{- end }}
{{- if and .Values.redis.external.password (not .Values.redis.external.existingSecret) }} {{- if and .Values.redis.external.password (not .Values.redis.external.existingSecret) }}
{{ .Values.redis.external.passwordKey | default "redis-password" }}: {{ .Values.redis.external.password | b64enc }} {{ .Values.redis.external.passwordKey | default "redis-password" }}: {{ .Values.redis.external.password | b64enc }}
{{ .Values.redis.external.urlKey | default "redis-url" }}: {{ include "paperless-ngx.redis.url.withPassword" . | b64enc }}
{{- end }} {{- end }}
{{- if and .Values.config.admin.user (not .Values.config.admin.existingSecret) }} {{- if and .Values.config.admin.user (not .Values.config.admin.existingSecret) }}
{{ .Values.config.admin.userKey | default "admin-user" }}: {{ .Values.config.admin.user | b64enc }} {{ .Values.config.admin.userKey | default "admin-user" }}: {{ .Values.config.admin.user | b64enc }}

View File

@ -5,7 +5,7 @@ fullnameOverride: ""
## Image settings ## Image settings
image: image:
repository: ghcr.io/paperless-ngx/paperless-ngx repository: ghcr.io/paperless-ngx/paperless-ngx
tag: "2.18.4" tag: "2.20.3"
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
## Deployment settings ## Deployment settings
@ -65,6 +65,7 @@ persistence:
# Paperless data directory (search index, classification model, etc.) # Paperless data directory (search index, classification model, etc.)
data: data:
enabled: true enabled: true
existingClaim: ""
storageClass: "" storageClass: ""
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 1Gi size: 1Gi
@ -72,6 +73,7 @@ persistence:
# Paperless media directory (documents and thumbnails) # Paperless media directory (documents and thumbnails)
media: media:
enabled: true enabled: true
existingClaim: ""
storageClass: "" storageClass: ""
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 10Gi size: 10Gi
@ -79,6 +81,7 @@ persistence:
# Export directory (for exporting documents) # Export directory (for exporting documents)
export: export:
enabled: true enabled: true
existingClaim: ""
storageClass: "" storageClass: ""
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 1Gi size: 1Gi
@ -86,6 +89,7 @@ persistence:
# Consume directory (for importing documents) # Consume directory (for importing documents)
consume: consume:
enabled: true enabled: true
existingClaim: ""
storageClass: "" storageClass: ""
accessMode: ReadWriteOnce accessMode: ReadWriteOnce
size: 5Gi size: 5Gi
@ -161,9 +165,13 @@ redis:
# Authentication (leave empty if Redis has no auth) # Authentication (leave empty if Redis has no auth)
username: "" # Optional: Redis username (Redis 6.0+) username: "" # Optional: Redis username (Redis 6.0+)
# Use existingSecret for credentials if Redis has auth # Use existingSecret for credentials if Redis has auth
# NOTE: When using existingSecret, the secret MUST contain a key with the full Redis URL
# Format: redis://[username]:[password]@host:port/database
existingSecret: "" existingSecret: ""
passwordKey: "redis-password" urlKey: "redis-url" # Key in existingSecret containing the full Redis URL
passwordKey: "redis-password" # Key in existingSecret for password (for compatibility)
# Or set password directly (leave empty if no auth) # Or set password directly (leave empty if no auth)
# When using plain password, the full Redis URL will be auto-generated in the secret
password: "" password: ""
# Optional: Prefix for Redis keys and channels # Optional: Prefix for Redis keys and channels
# Useful for sharing one Redis server among multiple Paperless instances # Useful for sharing one Redis server among multiple Paperless instances

View File

@ -2,7 +2,7 @@ apiVersion: v2
name: qbittorrent-vpn name: qbittorrent-vpn
description: qBittorrent with Gluetun VPN sidecar for Kubernetes description: qBittorrent with Gluetun VPN sidecar for Kubernetes
type: application type: application
version: 0.0.1 version: 0.0.2
appVersion: 5.1.0 appVersion: 5.1.0
maintainers: maintainers:
- name: Richard Tomik - name: Richard Tomik

View File

@ -1,117 +1,52 @@
# qBittorrent with Gluetun VPN # qBittorrent VPN Helm Chart
A Helm chart for deploying qBittorrent with a Gluetun VPN sidecar container on Kubernetes. A Helm chart for deploying [qBittorrent](https://www.qbittorrent.org/) with a [Gluetun](https://github.com/qdm12/gluetun) VPN sidecar on Kubernetes.
## Introduction ## Introduction
This chart deploys [qBittorrent](https://www.qbittorrent.org/) alongside [Gluetun](https://github.com/qdm12/gluetun), a VPN client/tunnel in a container, to ensure all BitTorrent traffic is routed through the VPN. The chart supports all major VPN providers and protocols through Gluetun's comprehensive compatibility. This chart deploys qBittorrent alongside Gluetun, ensuring all BitTorrent traffic is routed through a VPN. It supports 30+ VPN providers via OpenVPN or WireGuard, includes a kill-switch firewall, and exposes HTTP/Socks proxy services.
Source code can be found here: Source code: https://github.com/rtomik/helm-charts/tree/main/charts/qbittorrent-vpn
- https://github.com/rtomik/helm-charts/tree/main/charts/qbittorrent-vpn
Note: Currently only tested with NordVPN an OpenVPN configuration. **Note**: Currently only tested with NordVPN and OpenVPN configuration.
## Features
- **Multiple VPN Providers**: Support for 30+ VPN providers including NordVPN, ProtonVPN, Private Internet Access, ExpressVPN, Surfshark, Mullvad, and more
- **Protocol Support**: Use OpenVPN or WireGuard based on your provider's capabilities
- **Server Selection**: Choose servers by country, city, or specific hostnames with optional randomization
- **Security**: Proper container security settings to ensure traffic only flows through the VPN
- **Health Monitoring**: Integrated health checks for both qBittorrent and the VPN connection
- **Persistence**: Separate volume storage for configuration and downloads
- **Web UI**: Access qBittorrent via web interface with optional ingress support
- **Proxy Services**: HTTP and Shadowsocks proxies for additional devices to use the VPN tunnel
## Prerequisites ## Prerequisites
- Kubernetes 1.19+ - Kubernetes 1.19+
- Helm 3.2.0+ - Helm 3.2.0+
- PV provisioner support in the cluster - PV provisioner support in the cluster
- A valid subscription to a VPN service - A valid VPN subscription
## Installation ## Installing the Chart
### Add the Repository
```bash ```bash
helm repo add rtomik-charts https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
helm repo update helm install qbittorrent-vpn rtomik/qbittorrent-vpn
``` ```
### Create a Secret for VPN Credentials ## Uninstalling the Chart
For better security, store your VPN credentials in a Kubernetes secret:
```bash ```bash
# For OpenVPN authentication helm uninstall qbittorrent-vpn
kubectl create secret generic vpn-credentials \
--namespace default \
--from-literal=username='your-vpn-username' \
--from-literal=password='your-vpn-password'
# For WireGuard authentication (if using WireGuard)
kubectl create secret generic wireguard-keys \
--namespace default \
--from-literal=private_key='your-wireguard-private-key'
``` ```
Then reference this secret in your values: **Note**: PVCs are not deleted automatically. To remove them:
```yaml
gluetun:
credentials:
create: false
existingSecret: "vpn-credentials"
usernameKey: "username"
passwordKey: "password"
```
### Install the Chart
```bash
# Option 1: Installation with custom values file (recommended)
helm install qbittorrent-vpn rtomik-charts/qbittorrent-vpn -f values.yaml -n media
# Option 2: Installation with inline parameter overrides
helm install qbittorrent-vpn rtomik-charts/qbittorrent-vpn -n media \
--set gluetun.vpn.provider=nordvpn \
--set gluetun.vpn.serverCountries=Germany \
--set-string gluetun.credentials.existingSecret=vpn-credentials
```
## Uninstallation
```bash
helm uninstall qbittorrent-vpn -n media
```
Note: This will not delete Persistent Volume Claims. To delete them:
```bash ```bash
kubectl delete pvc -l app.kubernetes.io/instance=qbittorrent-vpn kubectl delete pvc -l app.kubernetes.io/instance=qbittorrent-vpn
``` ```
## Configuration ## Configuration Examples
### Key Parameters ### NordVPN with Existing Secret
| Parameter | Description | Default | First, create a secret with your VPN credentials:
|---------------------------------------|-------------------------------------------------------|----------------------------|
| `qbittorrent.image.repository` | qBittorrent image repository | `linuxserver/qbittorrent` |
| `qbittorrent.image.tag` | qBittorrent image tag | `latest` |
| `gluetun.image.repository` | Gluetun image repository | `qmcgaw/gluetun` |
| `gluetun.image.tag` | Gluetun image tag | `v3.40.0` |
| `gluetun.vpn.provider` | VPN provider name | `nordvpn` |
| `gluetun.vpn.type` | VPN protocol (`openvpn` or `wireguard`) | `openvpn` |
| `gluetun.vpn.serverCountries` | Countries to connect to (comma-separated) | `Germany` |
| `persistence.config.size` | Size of PVC for qBittorrent config | `2Gi` |
| `persistence.downloads.size` | Size of PVC for downloads | `100Gi` |
| `ingress.enabled` | Enable ingress controller resource | `true` |
| `ingress.hosts[0].host` | Hostname for the ingress | `qbittorrent.domain.com` |
For a complete list of parameters, see the [values.yaml](values.yaml) file. ```bash
kubectl create secret generic vpn-credentials \
### Example: Using with NordVPN --from-literal=username='your-vpn-username' \
--from-literal=password='your-vpn-password'
```
```yaml ```yaml
gluetun: gluetun:
@ -120,14 +55,26 @@ gluetun:
type: "openvpn" type: "openvpn"
serverCountries: "United States" serverCountries: "United States"
openvpn: openvpn:
NORDVPN_CATEGORY: "P2P" # For torrent-optimized servers NORDVPN_CATEGORY: "P2P"
credentials: credentials:
create: true create: false
username: "your-nordvpn-username" existingSecret: "vpn-credentials"
password: "your-nordvpn-password" usernameKey: "username"
passwordKey: "password"
ingress:
enabled: true
hosts:
- host: qbittorrent.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- qbittorrent.example.com
``` ```
### Example: Using with ProtonVPN ### ProtonVPN
```yaml ```yaml
gluetun: gluetun:
@ -136,15 +83,15 @@ gluetun:
type: "openvpn" type: "openvpn"
serverCountries: "Switzerland" serverCountries: "Switzerland"
openvpn: openvpn:
PROTONVPN_TIER: "2" # 0 is free, 2 is paid (Plus/Visionary) PROTONVPN_TIER: "2"
SERVER_FEATURES: "p2p" # For torrent support SERVER_FEATURES: "p2p"
credentials: credentials:
create: true create: true
username: "protonvpn-username" username: "protonvpn-username"
password: "protonvpn-password" password: "protonvpn-password"
``` ```
### Example: Using with Private Internet Access ### Private Internet Access with Port Forwarding
```yaml ```yaml
gluetun: gluetun:
@ -157,40 +104,11 @@ gluetun:
username: "pia-username" username: "pia-username"
password: "pia-password" password: "pia-password"
settings: settings:
VPN_PORT_FORWARDING: "on" # PIA supports port forwarding VPN_PORT_FORWARDING: "on"
STATUS_FILE: "/tmp/gluetun-status.json"
``` ```
## VPN Provider Support ### Proxy Services
This chart supports all VPN providers compatible with Gluetun, including:
- AirVPN
- Cyberghost
- ExpressVPN
- FastestVPN
- HideMyAss
- IPVanish
- IVPN
- Mullvad
- NordVPN
- Perfect Privacy
- Private Internet Access (PIA)
- PrivateVPN
- ProtonVPN
- PureVPN
- Surfshark
- TorGuard
- VyprVPN
- WeVPN
- Windscribe
For the complete list and provider-specific options, see the [Gluetun Providers Documentation](https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers).
## Additional Features
### Accessing the HTTP Proxy
Gluetun provides an HTTP proxy on port 8888 that can be used by other applications to route traffic through the VPN. To expose this proxy:
```yaml ```yaml
service: service:
@ -200,45 +118,168 @@ service:
socksPort: 8388 socksPort: 8388
``` ```
### Firewall Settings ### Custom Sidecar (NATMap)
By default, the chart enables the Gluetun firewall to prevent leaks if the VPN connection drops. You can customize this: Sidecars can access shared volumes: `config`, `downloads`, and `gluetun-config`.
```yaml ```yaml
gluetun: sidecars:
settings: - name: natmap
FIREWALL: "on" image: ghcr.io/muink/natmap:latest
FIREWALL_OUTBOUND_SUBNETS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" imagePullPolicy: IfNotPresent
env:
- name: GATEWAY
value: "10.2.0.1"
- name: INTERFACE
value: "tun0"
- name: INTERVAL
value: "30"
volumeMounts:
- name: config
mountPath: /config
subPath: natmap
``` ```
### Port Forwarding ## Parameters
For VPN providers that support port forwarding (like PIA): ### qBittorrent Parameters
```yaml | Name | Description | Default |
gluetun: |------|-------------|---------|
settings: | `qbittorrent.image.repository` | qBittorrent image repository | `linuxserver/qbittorrent` |
VPN_PORT_FORWARDING: "on" | `qbittorrent.image.tag` | qBittorrent image tag | `5.1.0` |
STATUS_FILE: "/tmp/gluetun-status.json" | `qbittorrent.image.pullPolicy` | Image pull policy | `IfNotPresent` |
``` | `qbittorrent.bittorrentPort` | BitTorrent traffic port | `6881` |
| `qbittorrent.service.port` | Web UI port | `8080` |
### Gluetun VPN Parameters
| Name | Description | Default |
|------|-------------|---------|
| `gluetun.enabled` | Enable Gluetun VPN sidecar | `true` |
| `gluetun.image.repository` | Gluetun image repository | `qmcgaw/gluetun` |
| `gluetun.image.tag` | Gluetun image tag | `v3.40.0` |
| `gluetun.vpn.provider` | VPN provider name | `nordvpn` |
| `gluetun.vpn.type` | VPN protocol (`openvpn` or `wireguard`) | `openvpn` |
| `gluetun.vpn.serverCountries` | Countries to connect (comma-separated) | `Netherlands` |
| `gluetun.vpn.serverCities` | Cities to connect (optional) | `""` |
| `gluetun.vpn.serverNames` | Specific server names (optional) | `""` |
| `gluetun.vpn.randomize` | Randomize server selection | `true` |
### VPN Credentials
| Name | Description | Default |
|------|-------------|---------|
| `gluetun.credentials.create` | Create credentials secret | `true` |
| `gluetun.credentials.username` | VPN username (if creating secret) | `""` |
| `gluetun.credentials.password` | VPN password (if creating secret) | `""` |
| `gluetun.credentials.existingSecret` | Existing secret name | `""` |
| `gluetun.credentials.usernameKey` | Key for username in secret | `username` |
| `gluetun.credentials.passwordKey` | Key for password in secret | `password` |
### Gluetun Settings
| Name | Description | Default |
|------|-------------|---------|
| `gluetun.settings.FIREWALL` | Enable firewall | `on` |
| `gluetun.settings.FIREWALL_OUTBOUND_SUBNETS` | Allowed outbound subnets | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` |
| `gluetun.settings.FIREWALL_INPUT_PORTS` | Ports allowed through firewall | `8080` |
| `gluetun.settings.FIREWALL_DEBUG` | Enable firewall debug | `on` |
| `gluetun.settings.VPN_PORT_FORWARDING` | Enable port forwarding | `off` |
| `gluetun.settings.DNS_ADDRESS` | DNS server address | `1.1.1.1` |
| `gluetun.resources.limits.cpu` | CPU limit | `300m` |
| `gluetun.resources.limits.memory` | Memory limit | `256Mi` |
### WireGuard Configuration (when using WireGuard)
| Name | Description | Default |
|------|-------------|---------|
| `gluetun.vpn.wireguard.privateKey` | WireGuard private key | `""` |
| `gluetun.vpn.wireguard.privateKeyExistingSecret` | Existing secret with private key | `""` |
| `gluetun.vpn.wireguard.addresses` | WireGuard addresses | `""` |
| `gluetun.vpn.wireguard.endpointIP` | Server endpoint IP (optional) | `""` |
| `gluetun.vpn.wireguard.endpointPort` | Server endpoint port (optional) | `""` |
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `podSecurityContext.runAsNonRoot` | Run as non-root | `false` |
| `podSecurityContext.runAsUser` | User ID | `0` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `0` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `8080` |
| `service.proxies.enabled` | Enable HTTP/Socks proxy services | `false` |
| `service.proxies.httpPort` | HTTP proxy port | `8888` |
| `service.proxies.socksPort` | Socks proxy port | `8388` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | `[]` |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `qbittorrent.persistence.config.enabled` | Enable config PVC | `true` |
| `qbittorrent.persistence.config.existingClaim` | Existing config PVC | `""` |
| `qbittorrent.persistence.config.size` | Config PVC size | `2Gi` |
| `qbittorrent.persistence.downloads.enabled` | Enable downloads PVC | `true` |
| `qbittorrent.persistence.downloads.existingClaim` | Existing downloads PVC | `""` |
| `qbittorrent.persistence.downloads.size` | Downloads PVC size | `2Gi` |
| `gluetun.persistence.enabled` | Enable Gluetun config PVC | `true` |
| `gluetun.persistence.size` | Gluetun config PVC size | `100Mi` |
### Sidecar Parameters
| Name | Description | Default |
|------|-------------|---------|
| `sidecars` | Additional sidecar containers | `[]` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Liveness probe path | `/` |
| `probes.liveness.periodSeconds` | Liveness period | `30` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/` |
| `probes.readiness.periodSeconds` | Readiness period | `10` |
## Supported VPN Providers
AirVPN, Cyberghost, ExpressVPN, FastestVPN, HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Private Internet Access, PrivateVPN, ProtonVPN, PureVPN, Surfshark, TorGuard, VyprVPN, WeVPN, Windscribe, and more.
See the [Gluetun Providers Documentation](https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers) for the full list and provider-specific options.
## Troubleshooting ## Troubleshooting
### VPN Connection Issues ### VPN Not Connecting
If the VPN isn't connecting properly:
1. Check the Gluetun logs:
```bash ```bash
kubectl logs deployment/qbittorrent-vpn -c gluetun kubectl logs deployment/qbittorrent-vpn -c gluetun
```
2. Verify your credentials are correct:
```bash
kubectl describe secret vpn-credentials kubectl describe secret vpn-credentials
``` ```
3. Try setting the log level to debug for more detailed information: Enable debug logging for more detail:
```yaml ```yaml
gluetun: gluetun:
extraEnv: extraEnv:
@ -246,28 +287,17 @@ If the VPN isn't connecting properly:
value: "debug" value: "debug"
``` ```
### qBittorrent Can't Create Directories ### Directory Creation Errors
If you see errors like "Could not create required directory": Ensure the init container is enabled and `fsGroup` is set in `podSecurityContext`.
1. Make sure the init container is enabled and properly configured ### Firewall / Network Issues
2. Ensure proper `fsGroup` is set in the `podSecurityContext`
3. Check that the persistence volume allows the correct permissions
### Firewall/Security Issues Gluetun requires `privileged: true` and `NET_ADMIN` capability. Verify `/dev/net/tun` is mounted correctly.
If you encounter iptables or network issues: ## Links
1. Ensure the Gluetun container has `privileged: true` - [qBittorrent](https://www.qbittorrent.org/)
2. Verify the `NET_ADMIN` capability is added - [Gluetun GitHub](https://github.com/qdm12/gluetun)
3. Check that the `/dev/net/tun` device is correctly mounted - [Gluetun Provider Setup](https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/qbittorrent-vpn)
## License
This chart is licensed under the MIT License.
## Acknowledgements
- [Gluetun](https://github.com/qdm12/gluetun) by [qdm12](https://github.com/qdm12)
- [LinuxServer.io](https://linuxserver.io/) for the qBittorrent container
- The qBittorrent team for the excellent torrent client

View File

@ -256,6 +256,10 @@ spec:
resources: resources:
{{- toYaml .Values.qbittorrent.resources | nindent 12 }} {{- toYaml .Values.qbittorrent.resources | nindent 12 }}
{{- with .Values.sidecars }}
{{- toYaml . | nindent 8 }}
{{- end }}
volumes: volumes:
# Create /dev/net/tun as a device # Create /dev/net/tun as a device
- name: tun - name: tun

View File

@ -226,3 +226,20 @@ extraVolumes: []
# Temporary options for development/debugging # Temporary options for development/debugging
hostNetwork: false hostNetwork: false
initContainers: [] initContainers: []
# Additional sidecar containers
# This allows you to add custom sidecar containers to the pod
# Each sidecar is specified using standard Kubernetes container spec
# Example: Add NATMap for port forwarding with VPN
# sidecars:
# - name: natmap
# image: ghcr.io/muink/natmap:latest
# env:
# - name: GATEWAY
# value: "10.2.0.1"
# - name: INTERFACE
# value: "tun0"
# volumeMounts:
# - name: config
# mountPath: /config
sidecars: []

View File

@ -1,140 +1,43 @@
# Recipya Helm Chart # Recipya Helm Chart
A Helm chart for deploying [Recipya](https://github.com/reaper47/recipya) on Kubernetes. A Helm chart for deploying [Recipya](https://github.com/reaper47/recipya), a recipe management application, on Kubernetes.
[Source Code](https://github.com/rtomik/helm-charts/tree/main/charts%2Frecipya)
## Introduction ## Introduction
This chart deploys Recipya recipe manager on a Kubernetes cluster using the Helm package manager. This chart deploys Recipya on a Kubernetes cluster using the Helm package manager. Recipya includes optimized Traefik ingress configuration with Content Security Policy support and sticky session handling for authentication.
Source code can be found here: Source code: https://github.com/rtomik/helm-charts/tree/main/charts/recipya
- https://github.com/rtomik/helm-charts/tree/main/charts/recipya
## Prerequisites ## Prerequisites
- Kubernetes 1.19+ - Kubernetes 1.19+
- Helm 3.2.0+ - Helm 3.2.0+
- PV provisioner support in the underlying infrastructure (if persistence is needed) - PV provisioner support (if persistence is needed)
## Installing the Chart ## Installing the Chart
To install the chart with the release name `recipya`:
```bash ```bash
helm repo add recipya-chart https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
helm install recipya recipya-chart/recipya -n recipya helm install recipya rtomik/recipya
``` ```
The command deploys Recipya on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation.
## Uninstalling the Chart ## Uninstalling the Chart
To uninstall/delete the `recipya` deployment:
```bash ```bash
helm uninstall recipya -n recipya helm uninstall recipya
``` ```
## Important Configuration Notes ## Configuration Examples
### Server URL ### Minimal Installation
When deploying with an ingress, it's **critical** to set `config.server.url` to match your ingress URL (including https if you're using TLS). This ensures that redirects after login work correctly: > **Important**: Set `config.server.url` to match your ingress URL including the scheme. This is required for post-login redirects to work correctly.
```yaml ```yaml
config: config:
server: server:
url: "https://your-recipya-domain.com" url: "https://recipya.example.com"
```
### Ingress Configuration
This chart includes optimized ingress configurations for Traefik, with support for WebSockets and proper security headers. If you're using a different ingress controller, you may need to adjust annotations accordingly.
## Parameters
### Global parameters
| Name | Description | Value |
|--------------------------|--------------------------------------|-----------------|
| `image.repository` | Recipya image repository | `reaper99/recipya` |
| `image.tag` | Recipya image tag | `v1.2.2` |
| `image.pullPolicy` | Recipya image pull policy | `IfNotPresent` |
| `replicaCount` | Number of Recipya replicas | `1` |
| `revisionHistoryLimit` | Number of revisions to keep | `3` |
### Security parameters
| Name | Description | Value |
|-----------------------------------------|--------------------------------------------------|-----------|
| `podSecurityContext.fsGroup` | Group ID for the Recipya container | `1000` |
| `containerSecurityContext` | Security context for the container | `{}` |
### Recipya configuration parameters
| Name | Description | Value |
|-----------------------------------------|-------------------------------------------------------|---------------------|
| `config.server.port` | Server port | `8078` |
| `config.server.autologin` | Whether to login automatically | `false` |
| `config.server.is_demo` | Whether the app is a demo version | `false` |
| `config.server.is_prod` | Whether the app is in production | `false` |
| `config.server.no_signups` | Whether to disable user account registrations | `false` |
| `config.server.url` | Base URL for the application | `http://0.0.0.0` |
| `config.email.address` | The email address for SendGrid | `""` |
| `config.email.sendgrid` | SendGrid API key | `""` |
| `config.documentIntelligence.endpoint` | Azure Document Intelligence endpoint | `""` |
| `config.documentIntelligence.key` | Azure Document Intelligence key | `""` |
### Service parameters
| Name | Description | Value |
|--------------------------|--------------------------------------------------|-------------|
| `service.type` | Recipya service type | `ClusterIP` |
| `service.port` | Recipya service port | `8078` |
### Ingress parameters
| Name | Description | Value |
|-------------------------------|--------------------------------------------------|------------------------|
| `ingress.enabled` | Enable ingress controller resource | `false` |
| `ingress.className` | IngressClass that will be used | `"traefik"` |
| `ingress.annotations` | Additional ingress annotations | See values.yaml |
| `ingress.hosts[0].host` | Default host for the ingress resource | `chart-example.local` |
| `ingress.tls` | TLS configuration | `[]` |
### Persistence parameters
| Name | Description | Value |
|--------------------------------------|------------------------------------------|------------------|
| `persistence.enabled` | Enable persistence using PVC | `true` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` | PVC Storage Request | `1Gi` |
| `persistence.storageClass` | Storage class of backing PVC | `""` |
### Resource parameters
| Name | Description | Value |
|-------------------------------|------------------------------------------|-----------|
| `resources.limits.cpu` | CPU limit | `500m` |
| `resources.limits.memory` | Memory limit | `512Mi` |
| `resources.requests.cpu` | CPU request | `100m` |
| `resources.requests.memory` | Memory request | `128Mi` |
### Probe parameters
| Name | Description | Value |
|--------------------------------------|--------------------------------------------|-----------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Path for liveness probe | `/` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Path for readiness probe | `/` |
## Traefik Ingress Configuration
The chart includes specially configured middlewares for Traefik to ensure proper functioning of Recipya:
```yaml
ingress: ingress:
enabled: true enabled: true
className: "traefik" className: "traefik"
@ -153,34 +56,16 @@ ingress:
- recipya.example.com - recipya.example.com
``` ```
This configuration includes: ### With SendGrid Email
1. Custom Content Security Policy allowing essential scripts from unpkg.com
2. Sticky sessions for maintaining authentication
3. Proper headers for proxy operation
## Content Security Policy Configuration
The chart includes a custom middleware that configures the proper Content Security Policy for Recipya. This is particularly important as the application requires access to external scripts from unpkg.com:
```yaml ```yaml
contentSecurityPolicy: >- config:
default-src 'self'; email:
script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: https://unpkg.com; address: "your-email@example.com"
style-src 'self' 'unsafe-inline'; sendgrid: "SG.your-sendgrid-api-key"
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' ws: wss: *;
worker-src 'self' blob:;
frame-src 'self';
media-src 'self' blob:;
object-src 'none';
form-action 'self';
``` ```
## Using Existing Secrets ### With SendGrid and Azure Document Intelligence via Existing Secrets
If you want to use existing secrets for sensitive data:
```yaml ```yaml
config: config:
@ -194,6 +79,127 @@ config:
keyKey: "di_key" keyKey: "di_key"
``` ```
## Configuration ## Parameters
See the [Recipya documentation](https://recipes.musicavis.ca/docs/installation/docker/#environment-variables) for details on all available configuration options. ### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Recipya image repository | `reaper99/recipya` |
| `image.tag` | Image tag | `v1.2.2` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Image pull secrets | `[]` |
### Pod Security Parameters
| Name | Description | Default |
|------|-------------|---------|
| `podSecurityContext.fsGroup` | Filesystem group ID | `1000` |
| `containerSecurityContext` | Container security context | `{}` |
### Application Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.server.port` | Server port | `8078` |
| `config.server.url` | Base URL (must match ingress) | `http://0.0.0.0` |
| `config.server.autologin` | Auto-login | `false` |
| `config.server.is_demo` | Demo mode | `false` |
| `config.server.is_prod` | Production mode | `true` |
| `config.server.no_signups` | Disable user registration | `false` |
| `config.email.address` | SendGrid email address | `""` |
| `config.email.sendgrid` | SendGrid API key | `""` |
| `config.email.existingSecret` | Existing secret for email | `""` |
| `config.email.addressKey` | Key for email address in secret | `email` |
| `config.email.sendgridKey` | Key for SendGrid key in secret | `sendgrid` |
| `config.documentIntelligence.endpoint` | Azure Document Intelligence endpoint | `""` |
| `config.documentIntelligence.key` | Azure Document Intelligence key | `""` |
| `config.documentIntelligence.existingSecret` | Existing secret for Azure DI | `""` |
| `config.documentIntelligence.endpointKey` | Key for endpoint in secret | `di_endpoint` |
| `config.documentIntelligence.keyKey` | Key for API key in secret | `di_key` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `8078` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | `[]` |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.enabled` | Enable persistence | `false` |
| `persistence.storageClass` | Storage class | `""` |
| `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | PVC size | `5Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Resource Parameters
| Name | Description | Default |
|------|-------------|---------|
| `resources` | Resource limits and requests | `{}` |
### Health Check Parameters
| Name | Description | Default |
|------|-------------|---------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.path` | Liveness probe path | `/` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `30` |
| `probes.liveness.periodSeconds` | Liveness period | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `30` |
| `probes.readiness.periodSeconds` | Readiness period | `10` |
## Troubleshooting
### Post-Login Redirect Fails
Ensure `config.server.url` matches your ingress URL exactly, including the scheme (`https://`).
### Content Security Policy Errors
The chart includes a Traefik middleware with a CSP policy allowing scripts from `unpkg.com`. If using a different ingress controller, configure an equivalent CSP policy:
```
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: https://unpkg.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
connect-src 'self' ws: wss: *;
```
### Debugging
```bash
kubectl logs deployment/recipya -f
kubectl describe pod -l app.kubernetes.io/name=recipya
```
## Links
- [Recipya GitHub](https://github.com/reaper47/recipya)
- [Recipya Documentation](https://recipes.musicavis.ca/docs/installation/docker/#environment-variables)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/recipya)

View File

@ -1,30 +1,42 @@
# Tandoor Recipes Helm Chart # Tandoor Recipes Helm Chart
A Helm chart for deploying [Tandoor Recipes](https://github.com/TandoorRecipes/recipes) on Kubernetes. A Helm chart for deploying [Tandoor Recipes](https://github.com/TandoorRecipes/recipes), a recipe management application, on Kubernetes.
Tandoor is a recipe management application that allows you to manage your recipes, plan meals, and create shopping lists. ## Introduction
Source code can be found here: This chart deploys Tandoor Recipes on a Kubernetes cluster. Tandoor supports PostgreSQL databases, LDAP/OIDC authentication, S3 object storage, email notifications, AI features, and Food Data Central API integration for nutrition data.
- https://github.com/rtomik/helm-charts/tree/main/charts/tandoor
Source code: https://github.com/rtomik/helm-charts/tree/main/charts/tandoor
## Prerequisites ## Prerequisites
- Kubernetes 1.19+ - Kubernetes 1.19+
- Helm 3.0+ - Helm 3.0+
- PV provisioner support in the underlying infrastructure - **External PostgreSQL database** (required — this chart does not include PostgreSQL)
- **External PostgreSQL database** (required - this chart does NOT include PostgreSQL) - PV provisioner support
## Installing the Chart ## Installing the Chart
```bash ```bash
helm repo add rtomik https://rtomik.github.io/helm-charts helm repo add rtomik https://rtomik.github.io/helm-charts
helm repo update helm install tandoor rtomik/tandoor
helm install tandoor rtomik/tandoor -f values.yaml
``` ```
## Usage Examples ## Uninstalling the Chart
### Minimal Configuration ```bash
helm uninstall tandoor
```
**Note**: PVCs are not deleted automatically. To remove them:
```bash
kubectl delete pvc -l app.kubernetes.io/name=tandoor
```
## Configuration Examples
### Minimal Installation
```yaml ```yaml
postgresql: postgresql:
@ -38,7 +50,7 @@ config:
value: "your-secret-key-at-least-50-characters-long-for-security-purposes" value: "your-secret-key-at-least-50-characters-long-for-security-purposes"
``` ```
### Production Configuration ### Production with Existing Secrets
```yaml ```yaml
postgresql: postgresql:
@ -52,20 +64,10 @@ config:
secretKey: secretKey:
existingSecret: "tandoor-app-secret" existingSecret: "tandoor-app-secret"
secretKey: "secret-key" secretKey: "secret-key"
allowedHosts: "tandoor.example.com" allowedHosts: "tandoor.example.com"
csrfTrustedOrigins: "https://tandoor.example.com" csrfTrustedOrigins: "https://tandoor.example.com"
timezone: "Europe/Berlin" timezone: "Europe/Berlin"
# Optional: OpenID Connect with Authentik
# oidc:
# enabled: true
# providerId: "authentik"
# providerName: "Authentik"
# clientId: "your-client-id"
# clientSecret: "your-client-secret"
# serverUrl: "https://authentik.company/application/o/tandoor/.well-known/openid-configuration"
ingress: ingress:
enabled: true enabled: true
className: "nginx" className: "nginx"
@ -84,7 +86,6 @@ ingress:
persistence: persistence:
staticfiles: staticfiles:
enabled: true enabled: true
# existingClaim: "my-existing-pvc"
storageClass: "longhorn" storageClass: "longhorn"
size: 2Gi size: 2Gi
mediafiles: mediafiles:
@ -101,323 +102,349 @@ resources:
memory: 256Mi memory: 256Mi
``` ```
### OIDC with Authentik
## Configuration ```yaml
config:
oidc:
enabled: true
providerId: "authentik"
providerName: "Authentik"
clientId: "your-client-id"
clientSecret: "your-client-secret"
serverUrl: "https://authentik.company/application/o/tandoor/.well-known/openid-configuration"
```
All configuration options are based on the official Tandoor documentation: ### S3 Object Storage
https://docs.tandoor.dev/system/configuration/
The following table lists the configurable parameters and their default values. ```yaml
config:
### Global Parameters s3:
enabled: true
| Name | Description | Value | bucketName: "tandoor-media"
|------|-------------|-------| regionName: "us-east-1"
| `nameOverride` | String to partially override the release name | `""` | endpointUrl: "https://minio.example.com"
| `fullnameOverride` | String to fully override the release name | `""` | existingSecret: "tandoor-s3-secret"
```
### Image Parameters
| Name | Description | Value |
|------|-------------|-------|
| `image.repository` | Tandoor image repository | `vabene1111/recipes` |
| `image.tag` | Tandoor image tag | `2.3.5` |
| `image.pullPolicy` | Tandoor image pull policy | `IfNotPresent` |
### Deployment Parameters
| Name | Description | Value |
|------|-------------|-------|
| `replicaCount` | Number of Tandoor replicas | `1` |
| `revisionHistoryLimit` | Number of old ReplicaSets to retain | `3` |
### PostgreSQL Parameters
| Name | Description | Value |
|------|-------------|-------|
| `postgresql.host` | PostgreSQL host | `postgresql.default.svc.cluster.local` |
| `postgresql.port` | PostgreSQL port | `5432` |
| `postgresql.database` | PostgreSQL database name | `tandoor` |
| `postgresql.username` | PostgreSQL username | `tandoor` |
| `postgresql.password` | PostgreSQL password (not recommended for production) | `""` |
| `postgresql.existingSecret` | Existing secret with PostgreSQL credentials | `""` |
| `postgresql.passwordKey` | Key in existing secret for PostgreSQL password | `postgresql-password` |
### Security Configuration
| Name | Description | Value |
|------|-------------|-------|
| `config.secretKey.value` | Django secret key (at least 50 characters) | `""` |
| `config.secretKey.existingSecret` | Existing secret for Django secret key | `""` |
| `config.secretKey.secretKey` | Key in existing secret for Django secret key | `secret-key` |
| `config.allowedHosts` | Allowed hosts for HTTP Host Header validation | `*` |
| `config.csrfTrustedOrigins` | CSRF trusted origins | `""` |
| `config.corsAllowOrigins` | Enable CORS allow all origins | `false` |
### Server Configuration
| Name | Description | Value |
|------|-------------|-------|
| `config.tandoorPort` | Port where Tandoor exposes its web server | `8080` |
| `config.gunicornWorkers` | Number of Gunicorn worker processes | `3` |
| `config.gunicornThreads` | Number of Gunicorn threads per worker | `2` |
| `config.gunicornTimeout` | Gunicorn request timeout in seconds | `30` |
| `config.gunicornMedia` | Enable media serving via Gunicorn | `0` |
| `config.timezone` | Application timezone | `UTC` |
| `config.scriptName` | URL path base for subfolder deployments | `""` |
| `config.sessionCookieDomain` | Session cookie domain | `""` |
| `config.sessionCookieName` | Session cookie identifier | `sessionid` |
### Feature Configuration
| Name | Description | Value |
|------|-------------|-------|
| `config.enableSignup` | Allow user registration | `false` |
| `config.enableMetrics` | Enable Prometheus metrics at /metrics | `false` |
| `config.enablePdfExport` | Enable recipe PDF export | `false` |
| `config.sortTreeByName` | Sort keywords/foods alphabetically | `false` |
### Social Authentication
| Name | Description | Value |
|------|-------------|-------|
| `config.socialDefaultAccess` | Space ID for auto-joining new social auth users | `0` |
| `config.socialDefaultGroup` | Default group for new users (guest/user/admin) | `guest` |
| `config.socialProviders` | Comma-separated OAuth provider list | `""` |
| `config.socialAccountProviders` | SOCIALACCOUNT_PROVIDERS JSON (for complex setups) | `""` |
### OpenID Connect (OIDC) Configuration
| Name | Description | Value |
|------|-------------|-------|
| `config.oidc.enabled` | Enable OpenID Connect authentication | `false` |
| `config.oidc.providerId` | Provider ID (e.g., authentik, keycloak) | `authentik` |
| `config.oidc.providerName` | Display name on login page | `Authentik` |
| `config.oidc.clientId` | Client ID from OIDC provider | `""` |
| `config.oidc.clientSecret` | Client Secret from OIDC provider | `""` |
| `config.oidc.serverUrl` | OpenID Connect well-known configuration URL | `""` |
### LDAP Configuration
| Name | Description | Value |
|------|-------------|-------|
| `config.ldap.enabled` | Enable LDAP authentication | `false` |
| `config.ldap.serverUri` | LDAP server URI | `""` |
| `config.ldap.bindDn` | LDAP bind distinguished name | `""` |
| `config.ldap.bindPassword` | LDAP bind password | `""` |
| `config.ldap.userSearchBaseDn` | LDAP user search base | `""` |
| `config.ldap.tlsCacertFile` | LDAP TLS CA certificate file | `""` |
| `config.ldap.startTls` | Enable LDAP StartTLS | `false` |
| `config.ldap.existingSecret` | Existing secret for LDAP credentials | `""` |
| `config.ldap.bindPasswordKey` | Key in existing secret for LDAP password | `ldap-bind-password` |
### Remote User Authentication
| Name | Description | Value |
|------|-------------|-------|
| `config.remoteUserAuth` | Enable REMOTE-USER header authentication | `false` |
### Email Configuration ### Email Configuration
| Name | Description | Value | ```yaml
|------|-------------|-------| config:
| `config.email.host` | SMTP server hostname | `""` | email:
| `config.email.port` | SMTP server port | `25` | host: "smtp.example.com"
| `config.email.user` | SMTP authentication username | `""` | port: 587
| `config.email.password` | SMTP authentication password | `""` | useTls: true
| `config.email.useTls` | Enable TLS for email | `false` | defaultFrom: "tandoor@example.com"
| `config.email.useSsl` | Enable SSL for email | `false` | existingSecret: "tandoor-email-secret"
| `config.email.defaultFrom` | Default from email address | `webmaster@localhost` | passwordKey: "email-password"
```
### LDAP Authentication
```yaml
config:
ldap:
enabled: true
serverUri: "ldap://ldap.example.com"
bindDn: "cn=admin,dc=example,dc=com"
bindPassword: "bind-password"
userSearchBaseDn: "ou=users,dc=example,dc=com"
existingSecret: "tandoor-ldap-secret"
bindPasswordKey: "ldap-bind-password"
```
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Tandoor image repository | `vabene1111/recipes` |
| `image.tag` | Image tag | `2.3.5` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
### Deployment Parameters
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `podSecurityContext.fsGroup` | Filesystem group ID | `0` |
| `containerSecurityContext.runAsUser` | User ID | `0` |
| `containerSecurityContext.runAsGroup` | Group ID | `0` |
| `containerSecurityContext.allowPrivilegeEscalation` | Allow privilege escalation | `false` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `8080` |
### Ingress Parameters
| Name | Description | Default |
|------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
### PostgreSQL Configuration (Required)
| Name | Description | Default |
|------|-------------|---------|
| `postgresql.host` | PostgreSQL host | `postgresql.default.svc.cluster.local` |
| `postgresql.port` | PostgreSQL port | `5432` |
| `postgresql.database` | Database name | `tandoor` |
| `postgresql.username` | Username | `tandoor` |
| `postgresql.password` | Password | `""` |
| `postgresql.existingSecret` | Existing secret name | `""` |
| `postgresql.passwordKey` | Key for password in secret | `postgresql-password` |
### Security Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.secretKey.value` | Django secret key (min 50 chars) | `""` |
| `config.secretKey.existingSecret` | Existing secret for secret key | `""` |
| `config.secretKey.secretKey` | Key in secret | `secret-key` |
| `config.allowedHosts` | Allowed HTTP hosts | `*` |
| `config.csrfTrustedOrigins` | CSRF trusted origins | `""` |
| `config.corsAllowOrigins` | Allow all CORS origins | `false` |
### Server Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.tandoorPort` | Web server port | `8080` |
| `config.gunicornWorkers` | Gunicorn workers | `3` |
| `config.gunicornThreads` | Gunicorn threads per worker | `2` |
| `config.gunicornTimeout` | Gunicorn timeout (seconds) | `30` |
| `config.gunicornMedia` | Serve media via Gunicorn | `0` |
| `config.timezone` | Timezone | `UTC` |
| `config.scriptName` | URL path base for subfolder | `""` |
| `config.sessionCookieDomain` | Session cookie domain | `""` |
| `config.sessionCookieName` | Session cookie name | `sessionid` |
### Feature Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.enableSignup` | Allow user registration | `false` |
| `config.enableMetrics` | Enable Prometheus metrics | `false` |
| `config.enablePdfExport` | Enable PDF export | `false` |
| `config.sortTreeByName` | Sort keywords alphabetically | `false` |
### OIDC Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.oidc.enabled` | Enable OIDC | `false` |
| `config.oidc.providerId` | Provider ID | `authentik` |
| `config.oidc.providerName` | Provider display name | `Authentik` |
| `config.oidc.clientId` | Client ID | `""` |
| `config.oidc.clientSecret` | Client secret | `""` |
| `config.oidc.serverUrl` | Well-known configuration URL | `""` |
### LDAP Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.ldap.enabled` | Enable LDAP | `false` |
| `config.ldap.serverUri` | LDAP server URI | `""` |
| `config.ldap.bindDn` | Bind DN | `""` |
| `config.ldap.bindPassword` | Bind password | `""` |
| `config.ldap.userSearchBaseDn` | User search base DN | `""` |
| `config.ldap.tlsCacertFile` | TLS CA cert file | `""` |
| `config.ldap.startTls` | Enable StartTLS | `false` |
| `config.ldap.existingSecret` | Existing secret for LDAP | `""` |
| `config.ldap.bindPasswordKey` | Key for bind password in secret | `ldap-bind-password` |
### Email Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.email.host` | SMTP host | `""` |
| `config.email.port` | SMTP port | `25` |
| `config.email.user` | SMTP username | `""` |
| `config.email.password` | SMTP password | `""` |
| `config.email.useTls` | Enable TLS | `false` |
| `config.email.useSsl` | Enable SSL | `false` |
| `config.email.defaultFrom` | Default from address | `webmaster@localhost` |
| `config.email.accountEmailSubjectPrefix` | Email subject prefix | `[Tandoor Recipes]` | | `config.email.accountEmailSubjectPrefix` | Email subject prefix | `[Tandoor Recipes]` |
| `config.email.existingSecret` | Existing secret for email credentials | `""` | | `config.email.existingSecret` | Existing secret for email | `""` |
| `config.email.passwordKey` | Key in existing secret for email password | `email-password` | | `config.email.passwordKey` | Key for password in secret | `email-password` |
### S3/Object Storage Configuration ### S3 Storage Configuration
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.s3.enabled` | Enable S3 storage for media files | `false` | | `config.s3.enabled` | Enable S3 storage | `false` |
| `config.s3.accessKey` | S3 access key | `""` | | `config.s3.accessKey` | S3 access key | `""` |
| `config.s3.secretAccessKey` | S3 secret access key | `""` | | `config.s3.secretAccessKey` | S3 secret access key | `""` |
| `config.s3.bucketName` | S3 bucket name | `""` | | `config.s3.bucketName` | S3 bucket name | `""` |
| `config.s3.regionName` | S3 region name | `""` | | `config.s3.regionName` | S3 region | `""` |
| `config.s3.endpointUrl` | Custom S3 endpoint URL (for MinIO) | `""` | | `config.s3.endpointUrl` | Custom S3 endpoint (MinIO) | `""` |
| `config.s3.customDomain` | CDN/proxy domain for S3 | `""` | | `config.s3.customDomain` | CDN/proxy domain | `""` |
| `config.s3.querystringAuth` | Use signed URLs for S3 objects | `true` | | `config.s3.querystringAuth` | Use signed URLs | `true` |
| `config.s3.querystringExpire` | Signed URL expiration (seconds) | `3600` | | `config.s3.querystringExpire` | Signed URL expiration (seconds) | `3600` |
| `config.s3.existingSecret` | Existing secret for S3 credentials | `""` | | `config.s3.existingSecret` | Existing secret for S3 | `""` |
### AI Features ### Social Authentication
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.socialDefaultAccess` | Space ID for auto-join | `0` |
| `config.socialDefaultGroup` | Default group (`guest`/`user`/`admin`) | `guest` |
| `config.socialProviders` | OAuth providers (comma-separated) | `""` |
| `config.remoteUserAuth` | Enable REMOTE-USER header auth | `false` |
### AI Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.ai.enabled` | Enable AI features | `false` | | `config.ai.enabled` | Enable AI features | `false` |
| `config.ai.creditsMonthly` | Monthly AI credits per space | `100` | | `config.ai.creditsMonthly` | Monthly credits per space | `100` |
| `config.ai.rateLimit` | AI API rate limit | `60/hour` | | `config.ai.rateLimit` | AI rate limit | `60/hour` |
### External Services ### External Services
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.fdcApiKey` | Food Data Central API key | `DEMO_KEY` | | `config.fdcApiKey` | Food Data Central API key | `DEMO_KEY` |
| `config.disableExternalConnectors` | Disable all external connectors | `false` | | `config.disableExternalConnectors` | Disable external connectors | `false` |
| `config.externalConnectorsQueueSize` | External connectors queue size | `100` | | `config.externalConnectorsQueueSize` | External connectors queue size | `100` |
### Rate Limiting ### Rate Limiting
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.ratelimitUrlImportRequests` | Rate limit for URL imports | `""` | | `config.ratelimitUrlImportRequests` | Rate limit for URL imports | `""` |
| `config.drfThrottleRecipeUrlImport` | DRF throttle for recipe URL import | `60/hour` | | `config.drfThrottleRecipeUrlImport` | DRF throttle for recipe URL import | `60/hour` |
### Space Defaults ### Space & User Defaults
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.spaceDefaultMaxRecipes` | Max recipes per space (0=unlimited) | `0` | | `config.spaceDefaultMaxRecipes` | Max recipes per space (0=unlimited) | `0` |
| `config.spaceDefaultMaxUsers` | Max users per space (0=unlimited) | `0` | | `config.spaceDefaultMaxUsers` | Max users per space (0=unlimited) | `0` |
| `config.spaceDefaultMaxFiles` | Max file storage in MB (0=unlimited) | `0` | | `config.spaceDefaultMaxFiles` | Max file storage in MB (0=unlimited) | `0` |
| `config.spaceDefaultAllowSharing` | Allow public recipe sharing | `true` | | `config.spaceDefaultAllowSharing` | Allow public sharing | `true` |
### User Preference Defaults
| Name | Description | Value |
|------|-------------|-------|
| `config.fractionPrefDefault` | Default fraction display | `false` | | `config.fractionPrefDefault` | Default fraction display | `false` |
| `config.commentPrefDefault` | Enable comments by default | `true` | | `config.commentPrefDefault` | Comments enabled by default | `true` |
| `config.stickyNavPrefDefault` | Sticky navbar by default | `true` | | `config.stickyNavPrefDefault` | Sticky navbar by default | `true` |
| `config.maxOwnedSpacesPrefDefault` | Max spaces per user | `100` | | `config.maxOwnedSpacesPrefDefault` | Max spaces per user | `100` |
### Cosmetic Configuration ### Performance & Cosmetic
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.shoppingMinAutosyncInterval` | Min auto-sync interval (minutes) | `5` |
| `config.exportFileCacheDuration` | Export cache duration (seconds) | `600` |
| `config.unauthenticatedThemeFromSpace` | Space ID for unauthenticated theme | `0` | | `config.unauthenticatedThemeFromSpace` | Space ID for unauthenticated theme | `0` |
| `config.forceThemeFromSpace` | Space ID to enforce theme globally | `0` | | `config.forceThemeFromSpace` | Space ID to enforce theme globally | `0` |
### Performance Configuration
| Name | Description | Value |
|------|-------------|-------|
| `config.shoppingMinAutosyncInterval` | Min auto-sync interval (minutes) | `5` |
| `config.exportFileCacheDuration` | Export cache duration (seconds) | `600` |
### Legal URLs ### Legal URLs
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.termsUrl` | Terms of service URL | `""` | | `config.termsUrl` | Terms of service URL | `""` |
| `config.privacyUrl` | Privacy policy URL | `""` | | `config.privacyUrl` | Privacy policy URL | `""` |
| `config.imprintUrl` | Legal imprint URL | `""` | | `config.imprintUrl` | Legal imprint URL | `""` |
### hCaptcha Configuration ### hCaptcha Configuration
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `config.hcaptcha.siteKey` | hCaptcha site key | `""` | | `config.hcaptcha.siteKey` | hCaptcha site key | `""` |
| `config.hcaptcha.secret` | hCaptcha secret key | `""` | | `config.hcaptcha.secret` | hCaptcha secret | `""` |
| `config.hcaptcha.existingSecret` | Existing secret for hCaptcha | `""` | | `config.hcaptcha.existingSecret` | Existing secret for hCaptcha | `""` |
### Debugging
| Name | Description | Value |
|------|-------------|-------|
| `config.debug` | Enable Django debug mode | `false` |
| `config.debugToolbar` | Enable Django Debug Toolbar | `false` |
| `config.sqlDebug` | Enable SQL debug output | `false` |
| `config.logLevel` | Application log level | `WARNING` |
| `config.gunicornLogLevel` | Gunicorn log level | `info` |
### Service Parameters
| Name | Description | Value |
|------|-------------|-------|
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `8080` |
### Ingress Parameters
| Name | Description | Value |
|------|-------------|-------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `""` |
| `ingress.annotations` | Ingress annotations | See values.yaml |
| `ingress.hosts` | Ingress hosts configuration | See values.yaml |
| `ingress.tls` | Ingress TLS configuration | See values.yaml |
### Persistence Parameters ### Persistence Parameters
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `persistence.staticfiles.enabled` | Enable static files persistence | `true` | | `persistence.staticfiles.enabled` | Enable static files PVC | `true` |
| `persistence.staticfiles.existingClaim` | Use existing PVC for static files | `""` | | `persistence.staticfiles.existingClaim` | Existing PVC for static files | `""` |
| `persistence.staticfiles.storageClass` | Storage class for static files | `""` | | `persistence.staticfiles.storageClass` | Storage class | `""` |
| `persistence.staticfiles.accessMode` | Access mode for static files PVC | `ReadWriteOnce` | | `persistence.staticfiles.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.staticfiles.size` | Size of static files PVC | `1Gi` | | `persistence.staticfiles.size` | PVC size | `1Gi` |
| `persistence.mediafiles.enabled` | Enable media files persistence | `true` | | `persistence.mediafiles.enabled` | Enable media files PVC | `true` |
| `persistence.mediafiles.existingClaim` | Use existing PVC for media files | `""` | | `persistence.mediafiles.existingClaim` | Existing PVC for media files | `""` |
| `persistence.mediafiles.storageClass` | Storage class for media files | `""` | | `persistence.mediafiles.storageClass` | Storage class | `""` |
| `persistence.mediafiles.accessMode` | Access mode for media files PVC | `ReadWriteOnce` | | `persistence.mediafiles.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.mediafiles.size` | Size of media files PVC | `5Gi` | | `persistence.mediafiles.size` | PVC size | `5Gi` |
### Pod Security Context ### Resource Parameters
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `podSecurityContext.runAsNonRoot` | Run as non-root user | `true` | | `resources` | Resource limits and requests | `{}` |
| `podSecurityContext.runAsUser` | User ID to run as | `1000` |
| `podSecurityContext.fsGroup` | Group ID for filesystem | `1000` |
### Container Security Context ### Health Check Parameters
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `containerSecurityContext.allowPrivilegeEscalation` | Allow privilege escalation | `false` | | `probes.liveness.enabled` | Enable liveness probe | `true` |
| `containerSecurityContext.readOnlyRootFilesystem` | Read-only root filesystem | `false` | | `probes.liveness.path` | Liveness probe path | `/` |
| `probes.liveness.initialDelaySeconds` | Liveness initial delay | `30` |
| `probes.liveness.periodSeconds` | Liveness period | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.path` | Readiness probe path | `/` |
| `probes.readiness.initialDelaySeconds` | Readiness initial delay | `15` |
| `probes.readiness.periodSeconds` | Readiness period | `5` |
### Autoscaling Parameters ### Autoscaling Parameters
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `autoscaling.enabled` | Enable autoscaling | `false` | | `autoscaling.enabled` | Enable HPA | `false` |
| `autoscaling.minReplicas` | Minimum replicas | `1` | | `autoscaling.minReplicas` | Min replicas | `1` |
| `autoscaling.maxReplicas` | Maximum replicas | `3` | | `autoscaling.maxReplicas` | Max replicas | `3` |
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU utilization | `80` | | `autoscaling.targetCPUUtilizationPercentage` | Target CPU | `80` |
| `autoscaling.targetMemoryUtilizationPercentage` | Target memory utilization | `80` | | `autoscaling.targetMemoryUtilizationPercentage` | Target memory | `80` |
### Probes Configuration ### Debugging
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `probes.liveness.enabled` | Enable liveness probe | `true` | | `config.debug` | Enable Django debug mode | `false` |
| `probes.liveness.initialDelaySeconds` | Initial delay for liveness probe | `30` | | `config.debugToolbar` | Enable Debug Toolbar | `false` |
| `probes.liveness.periodSeconds` | Period for liveness probe | `10` | | `config.sqlDebug` | Enable SQL debug | `false` |
| `probes.readiness.enabled` | Enable readiness probe | `true` | | `config.logLevel` | Application log level | `WARNING` |
| `probes.readiness.initialDelaySeconds` | Initial delay for readiness probe | `15` | | `config.gunicornLogLevel` | Gunicorn log level | `info` |
| `probes.readiness.periodSeconds` | Period for readiness probe | `5` |
### Additional Configuration ### Additional Configuration
| Name | Description | Value | | Name | Description | Default |
|------|-------------|-------| |------|-------------|---------|
| `env` | Additional environment variables | `[]` | | `env` | Extra environment variables | `[]` |
| `extraEnvFrom` | Additional environment variables from secrets | `[]` | | `extraEnvFrom` | Extra env from secrets/configmaps | `[]` |
| `extraVolumes` | Additional volumes | `[]` | | `extraVolumes` | Extra volumes | `[]` |
| `extraVolumeMounts` | Additional volume mounts | `[]` | | `extraVolumeMounts` | Extra volume mounts | `[]` |
| `nodeSelector` | Node selector | `{}` |
| `tolerations` | Tolerations | `[]` |
| `affinity` | Affinity rules | `{}` |
## Uninstalling the Chart ## Troubleshooting
- **CSRF Errors**: Set `config.csrfTrustedOrigins` to your domain URL including the scheme
- **Login Issues**: Verify OIDC/LDAP configuration and callback URLs
- **Missing Media**: Check persistence configuration and S3 connectivity if enabled
```bash ```bash
helm uninstall tandoor kubectl logs -f deployment/tandoor
``` kubectl describe pod -l app.kubernetes.io/name=tandoor
**Note:** PVCs are not automatically deleted. To remove them:
```bash
kubectl delete pvc -l app.kubernetes.io/name=tandoor
``` ```
## Links ## Links
@ -425,3 +452,4 @@ kubectl delete pvc -l app.kubernetes.io/name=tandoor
- [Tandoor Recipes GitHub](https://github.com/TandoorRecipes/recipes) - [Tandoor Recipes GitHub](https://github.com/TandoorRecipes/recipes)
- [Tandoor Documentation](https://docs.tandoor.dev/) - [Tandoor Documentation](https://docs.tandoor.dev/)
- [Configuration Reference](https://docs.tandoor.dev/system/configuration/) - [Configuration Reference](https://docs.tandoor.dev/system/configuration/)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/tandoor)