Compare commits

...

30 Commits

Author SHA1 Message Date
256f7e6807 version bump norish 2026-04-13 16:15:48 +02:00
839d5ce530 norish release 0.15.4 2026-02-03 19:42:29 +01:00
dbfd393db5 Helm chart for norish upgraded to work with new version 0.14.1
added redis config

when upgrading refer to readme file
2025-12-22 12:28:08 +01:00
0014e7498d resolved [Bug][Paperless] Redis password from secret isn't working #5 2025-12-19 10:39:07 +01:00
fe7c741bda reverted changes from previous commit 2025-12-19 09:04:15 +01:00
55d1ce8377 fixed [Bug][Paperless] Redis password from secret isn't working #5 2025-12-19 09:00:28 +01:00
d3cdd77cc6 added support for using existing pvc 2025-12-17 16:14:10 +01:00
4c8179f9cc fixed issue #2 2025-12-17 15:59:50 +01:00
7be50d4890 added option to add extra env variables 2025-12-11 13:05:15 +01:00
3a61591220 fixed bug with norish 2025-12-11 12:40:17 +01:00
25265eb94f deleted values file 2025-12-11 10:12:35 +01:00
8e34bd33dd release of norish 2025-12-08 17:02:52 +01:00
4cb45e3013 release paperless ngx 0.0.2 fixed bug with redis configuration 2025-10-26 18:28:25 +01:00
e65df72663 Merge branch 'main' of https://github.com/rtomik/helm-charts 2025-10-13 09:27:00 +02:00
33f865a892 edited example 2025-10-13 09:26:54 +02:00
2ecf4aeec0 Merge pull request #1 from piontec/add-pvc-selector
chg: add support for PVC selector
2025-10-11 14:40:57 +02:00
720a81d343 fixed bug with external postgresql v 1.0.4 2025-10-11 14:29:26 +02:00
c9b25918d5 chg: add support for PVC selector 2025-10-01 15:03:41 +02:00
c81bb1bbd1 fixed bug with db configuration donetick helm chart 2025-09-22 09:19:55 +02:00
741401a79d enhance donetick helm chart with missing features and repository standards… 2025-09-21 18:31:04 +02:00
509492560e mealie helm chart release with upgraded version 2025-09-20 19:31:52 +02:00
af1ecd86cb Release paperlessngx helmchart v0.0.1 2025-09-14 17:37:28 +02:00
85b6787314 Fixed bug with joplin server helm chart with secret values 2025-08-25 09:08:40 +02:00
e809d6067d joplin-server helmchart v0.0.1 2025-08-24 21:47:31 +02:00
7cb71b046c release 0.0.1 for mealie helm chart 2025-08-24 15:45:29 +02:00
fa186d389d release 0.0.1 for karakeep 2025-08-11 11:26:33 +02:00
cf9632473d edited readme 2025-05-15 12:22:28 +02:00
7b4e184104 Release v 0.0.1 helm chart for qbittorent and gluetun 2025-05-13 22:13:11 +02:00
c413bc7757 changed description of helm charts 2025-05-02 15:11:27 +02:00
05d7a2b0cd edited readme 2025-04-23 16:11:13 +02:00
80 changed files with 9248 additions and 368 deletions

View File

@ -1,9 +1,9 @@
apiVersion: v2
name: donetick
description: A Helm chart for Donetick application
description: Donetick helm chart for Kubernetes
type: application
version: 1.0.1
appVersion: "v0.1.38"
version: 1.0.6
appVersion: "v0.1.60"
maintainers:
- name: Richard Tomik
email: no@m.com

View File

@ -1,10 +1,12 @@
# 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
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: https://github.com/rtomik/helm-charts/tree/main/charts/donetick
## Prerequisites
@ -14,97 +16,353 @@ This chart deploys [Donetick](https://github.com/donetick/donetick) on a Kuberne
## Installing the Chart
To install the chart with the release name `donetick`:
```bash
$ helm repo add donetick-chart https://rtomik.github.io/helm-charts
$ helm install donetick donetick-chart/donetick
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install donetick rtomik/donetick
```
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart
To uninstall/delete the `donetick` deployment:
```bash
helm uninstall donetick
```
## Configuration Examples
### Minimal Installation (SQLite)
The chart works out of the box with SQLite — no additional configuration required:
```bash
$ helm uninstall donetick
helm install donetick rtomik/donetick
```
### 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
config:
database:
type: "postgres"
host: "postgresql.database.svc.cluster.local"
port: 5432
name: "donetick"
secrets:
existingSecret: "donetick-postgres-secret"
userKey: "username"
passwordKey: "password"
jwt:
existingSecret: "donetick-jwt-secret"
secretKey: "jwtSecret"
session_time: "168h"
oauth2:
existingSecret: "donetick-oauth-secret"
clientIdKey: "client-id"
clientSecretKey: "client-secret"
auth_url: "https://your-oauth-provider.com/auth"
token_url: "https://your-oauth-provider.com/token"
user_info_url: "https://your-oauth-provider.com/userinfo"
redirect_url: "https://donetick.your-domain.com/auth/callback"
server:
cors_allow_origins:
- "https://donetick.your-domain.com"
rate_limit: 100
rate_period: "60s"
features:
notifications: true
realtime: true
oauth: true
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
ingress:
enabled: true
hosts:
- host: donetick.your-domain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- donetick.your-domain.com
```
Create the required secrets:
```bash
kubectl create secret generic donetick-postgres-secret \
--from-literal=username='donetick' \
--from-literal=password='your-secure-db-password'
kubectl create secret generic donetick-jwt-secret \
--from-literal=jwtSecret='your-very-secure-jwt-secret-at-least-32-characters-long'
kubectl create secret generic donetick-oauth-secret \
--from-literal=client-id='your-oauth-client-id' \
--from-literal=client-secret='your-oauth-client-secret'
```
## Parameters
### Global parameters
### Global Parameters
| Name | Description | Value |
|------------------------|-------------------------------------------------------------------------------------|-------|
| `nameOverride` | String to partially override the release name | `""` |
| `fullnameOverride` | String to fully override the release name | `""` |
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
### Image parameters
### Image Parameters
| Name | Description | Value |
|-------------------------|--------------------------------------------------------------------------------------|--------------------|
| `image.repository` | Donetick image repository | `donetick/donetick` |
| `image.tag` | Donetick image tag | `latest` |
| `image.pullPolicy` | Donetick image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Global Docker registry secret names as an array | `[]` |
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Donetick image repository | `donetick/donetick` |
| `image.tag` | Image tag | `v0.1.60` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Image pull secrets | `[]` |
### Secret Management
### Deployment Parameters
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|---------------------|
| `config.jwt.existingSecret` | Name of existing secret for JWT token | `""` |
| `config.jwt.secretKey` | Key in the existing secret for JWT token | `"jwtSecret"` |
| `config.oauth2.existingSecret` | Name of existing secret for OAuth2 credentials | `""` |
| `config.oauth2.clientIdKey` | Key in the existing secret for OAuth2 client ID | `"client-id"` |
| `config.oauth2.clientSecretKey` | Key in the existing secret for OAuth2 client secret | `"client-secret"` |
| `config.database.existingSecret` | Name of existing secret for database credentials | `""` |
| `config.database.hostKey` | Key in the existing secret for database host | `"db-host"` |
| `config.database.portKey` | Key in the existing secret for database port | `"db-port"` |
| `config.database.userKey` | Key in the existing secret for database user | `"db-user"` |
| `config.database.passwordKey` | Key in the existing secret for database password | `"db-password"` |
| `config.database.nameKey` | Key in the existing secret for database name | `"db-name"` |
| Name | Description | Default |
|------|-------------|---------|
| `replicaCount` | Number of replicas | `1` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
| `startupArgs` | Optional startup arguments | `[]` |
| `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 | `{}` |
### Deployment parameters
### Service Parameters
| Name | Description | Value |
|--------------------------------------|--------------------------------------------------------------------------|-----------|
| `replicaCount` | Number of Donetick 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 | `{}` |
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `2021` |
| `service.annotations` | Service annotations | `{}` |
### Service parameters
### Ingress Parameters
| Name | Description | Value |
|----------------------------|------------------------------------------------------|-------------|
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `2021` |
| `service.annotations` | Additional annotations for Service | `{}` |
| `service.nodePort` | Service HTTP node port (when applicable) | `""` |
| 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 |
### Ingress parameters
### Persistence 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[].secretName` | Host-specific TLS secret name (overrides global) | `""` |
| Name | Description | Default |
|------|-------------|---------|
| `persistence.enabled` | Enable persistence | `false` |
| `persistence.storageClass` | Storage class | `""` |
| `persistence.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.size` | PVC size | `1Gi` |
| `persistence.annotations` | PVC annotations | `{}` |
### Persistence parameters
### Database Configuration
| Name | Description | Value |
|-------------------------------|------------------------------------------------------|---------------|
| `persistence.enabled` | Enable persistence using PVC | `true` |
| `persistence.storageClass` | PVC Storage Class | `"longhorn"` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` |
| Name | Description | Default |
|------|-------------|---------|
| `config.database.type` | Database type (`sqlite` or `postgres`) | `sqlite` |
| `config.database.migration` | Enable migrations | `true` |
| `config.database.migration_skip` | Skip migrations | `false` |
| `config.database.migration_retry` | Migration retry count | `3` |
| `config.database.migration_timeout` | Migration timeout | `600s` |
| `config.database.host` | PostgreSQL host | `""` |
| `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
| Name | Description | Default |
|------|-------------|---------|
| `config.jwt.secret` | JWT signing secret (min 32 chars) | `changeme-...` |
| `config.jwt.session_time` | Session 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
| Name | Description | Default |
|------|-------------|---------|
| `config.server.port` | Server port | `2021` |
| `config.server.read_timeout` | Read timeout | `10s` |
| `config.server.write_timeout` | Write timeout | `10s` |
| `config.server.rate_period` | Rate limiting period | `60s` |
| `config.server.rate_limit` | Rate limit per period | `300` |
| `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
| Name | Description | Default |
|------|-------------|---------|
| `config.features.notifications` | Enable notifications | `true` |
| `config.features.realtime` | Enable real-time features | `true` |
| `config.features.oauth` | Enable OAuth | `false` |
| `config.is_user_creation_disabled` | Disable user registration | `false` |
### 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.path` | Startup probe path | `/health` |
| `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` |
### Autoscaling Parameters
| Name | Description | Default |
|------|-------------|---------|
| `autoscaling.enabled` | Enable HPA | `false` |
| `autoscaling.minReplicas` | Min replicas | `1` |
| `autoscaling.maxReplicas` | Max replicas | `5` |
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU | `80` |
| `autoscaling.targetMemoryUtilizationPercentage` | Target memory | `80` |
## Troubleshooting
### Real-time Configuration Panic
**Error**: `Invalid real-time configuration: maxConnections must be positive, got 0`
Ensure `config.realtime.max_connections` is set to a positive value (default: `100`).
### Database Connection Issues
- Verify PostgreSQL is running and accessible
- Check database credentials in secrets
- Ensure the database exists
- Verify network policies allow the connection
### JWT Authentication Failures
Ensure `config.jwt.secret` is at least 32 characters long. For production, use `config.jwt.existingSecret`.
### CORS Issues
Add your domain to `config.server.cors_allow_origins`:
```yaml
config:
server:
cors_allow_origins:
- "https://your-domain.com"
```
### Debugging
```bash
kubectl logs deployment/donetick -f
kubectl get configmap donetick-configmap -o yaml
```
## Links
- [Donetick GitHub](https://github.com/donetick/donetick)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/donetick)

View File

@ -22,15 +22,18 @@ data:
{{- if .Values.config.database.migration_retry }}
migration_retry: {{ .Values.config.database.migration_retry }}
{{- end }}
migration_timeout: {{ .Values.config.database.migration_timeout | default "300s" | quote }}
{{- if eq .Values.config.database.type "postgres" }}
{{- if not .Values.config.database.existingSecret }}
host: {{ .Values.config.database.host | quote }}
port: {{ .Values.config.database.port }}
name: {{ .Values.config.database.name | quote }}
{{- if not .Values.config.database.secrets.existingSecret }}
user: {{ .Values.config.database.user | quote }}
password: {{ .Values.config.database.password | quote }}
name: {{ .Values.config.database.name | quote }}
{{- else }}
# Database credentials will be injected via environment variables from Secret
# Reference environment variables for database credentials
user: "$DT_DATABASE_USER"
password: "$DT_DATABASE_PASSWORD"
{{- end }}
{{- end }}
jwt:
@ -75,3 +78,19 @@ data:
user_info_url: {{ .Values.config.oauth2.user_info_url | default "" | quote }}
redirect_url: {{ .Values.config.oauth2.redirect_url | default "" | quote }}
name: {{ .Values.config.oauth2.name | default "" | quote }}
realtime:
max_connections: {{ .Values.config.realtime.max_connections }}
ping_interval: {{ .Values.config.realtime.ping_interval | quote }}
pong_wait: {{ .Values.config.realtime.pong_wait | quote }}
write_wait: {{ .Values.config.realtime.write_wait | quote }}
max_message_size: {{ .Values.config.realtime.max_message_size }}
logging:
level: {{ .Values.config.logging.level | quote }}
format: {{ .Values.config.logging.format | quote }}
storage:
type: {{ .Values.config.storage.type | quote }}
path: {{ .Values.config.storage.path | quote }}
features:
notifications: {{ .Values.config.features.notifications }}
realtime: {{ .Values.config.features.realtime }}
oauth: {{ .Values.config.features.oauth }}

View File

@ -50,6 +50,17 @@ spec:
- name: http
containerPort: {{ .Values.config.server.port }}
protocol: TCP
{{- if .Values.probes.startup.enabled }}
startupProbe:
httpGet:
path: {{ .Values.probes.startup.path }}
port: http
initialDelaySeconds: {{ .Values.probes.startup.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.startup.periodSeconds }}
timeoutSeconds: {{ .Values.probes.startup.timeoutSeconds }}
failureThreshold: {{ .Values.probes.startup.failureThreshold }}
successThreshold: {{ .Values.probes.startup.successThreshold }}
{{- end }}
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
@ -77,15 +88,44 @@ spec:
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- if or .Values.config.jwt.existingSecret .Values.config.oauth2.existingSecret .Values.config.database.existingSecret }}
# Secret-based environment variables
# Database configuration environment variables
{{- if eq .Values.config.database.type "postgres" }}
- name: DT_DATABASE_TYPE
value: "postgres"
- name: DT_DATABASE_HOST
value: {{ .Values.config.database.host | quote }}
- name: DT_DATABASE_PORT
value: {{ .Values.config.database.port | quote }}
- name: DT_DATABASE_NAME
value: {{ .Values.config.database.name | quote }}
{{- if .Values.config.database.secrets.existingSecret }}
- name: DT_DATABASE_USER
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.secrets.existingSecret }}
key: {{ .Values.config.database.secrets.userKey }}
- name: DT_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.secrets.existingSecret }}
key: {{ .Values.config.database.secrets.passwordKey }}
{{- end }}
{{- else }}
- name: DT_DATABASE_TYPE
value: {{ .Values.config.database.type | quote }}
{{- end }}
# JWT configuration
{{- if .Values.config.jwt.existingSecret }}
- name: DT_JWT_SECRET
valueFrom:
secretKeyRef:
name: {{ .Values.config.jwt.existingSecret }}
key: {{ .Values.config.jwt.secretKey }}
{{- else }}
- name: DT_JWT_SECRET
value: {{ .Values.config.jwt.secret | quote }}
{{- end }}
# OAuth2 configuration
{{- if .Values.config.oauth2.existingSecret }}
- name: DT_OAUTH2_CLIENT_ID
valueFrom:
@ -98,34 +138,6 @@ spec:
name: {{ .Values.config.oauth2.existingSecret }}
key: {{ .Values.config.oauth2.clientSecretKey }}
{{- end }}
{{- if and .Values.config.database.existingSecret (eq .Values.config.database.type "postgres") }}
- name: DT_DB_HOST
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.existingSecret }}
key: {{ .Values.config.database.hostKey }}
- name: DT_DB_PORT
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.existingSecret }}
key: {{ .Values.config.database.portKey }}
- name: DT_DB_USER
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.existingSecret }}
key: {{ .Values.config.database.userKey }}
- name: DT_DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.existingSecret }}
key: {{ .Values.config.database.passwordKey }}
- name: DT_DB_NAME
valueFrom:
secretKeyRef:
name: {{ .Values.config.database.existingSecret }}
key: {{ .Values.config.database.nameKey }}
{{- end }}
{{- end }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
@ -148,9 +160,14 @@ spec:
- name: config
configMap:
name: {{ include "donetick.fullname" . }}-configmap
{{- if .Values.persistence.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ include "donetick.fullname" . }}-data
{{- else }}
- name: data
emptyDir: {}
{{- end }}
{{- if not .Values.containerSecurityContext.readOnlyRootFilesystem }}
- name: tmp
emptyDir: {}

View File

@ -1,4 +1,4 @@
{{- if or (not .Values.config.jwt.existingSecret) (and (not .Values.config.oauth2.existingSecret) (or .Values.config.oauth2.client_id .Values.config.oauth2.client_secret)) (and (eq .Values.config.database.type "postgres") (not .Values.config.database.existingSecret)) }}
{{- if or (not .Values.config.jwt.existingSecret) (and (not .Values.config.oauth2.existingSecret) (or .Values.config.oauth2.client_id .Values.config.oauth2.client_secret)) (and (eq .Values.config.database.type "postgres") (not .Values.config.database.secrets.existingSecret)) }}
apiVersion: v1
kind: Secret
metadata:
@ -10,8 +10,8 @@ data:
{{- if not .Values.config.jwt.existingSecret }}
{{ .Values.config.jwt.secretKey }}: {{ .Values.config.jwt.secret | b64enc }}
{{- end }}
{{- if and (eq .Values.config.database.type "postgres") (not .Values.config.database.existingSecret) }}
{{ .Values.config.database.passwordKey }}: {{ .Values.config.database.password | b64enc }}
{{- if and (eq .Values.config.database.type "postgres") (not .Values.config.database.secrets.existingSecret) }}
{{ .Values.config.database.secrets.passwordKey }}: {{ .Values.config.database.password | b64enc }}
{{- end }}
{{- if and (not .Values.config.oauth2.existingSecret) .Values.config.oauth2.client_id }}
{{ .Values.config.oauth2.clientIdKey }}: {{ .Values.config.oauth2.client_id | b64enc }}

View File

@ -4,6 +4,10 @@ metadata:
name: {{ include "donetick.fullname" . }}
labels:
{{- include "donetick.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:

View File

@ -5,9 +5,11 @@ fullnameOverride: ""
## Image settings
image:
repository: donetick/donetick
tag: "v0.1.38"
tag: "v0.1.60"
pullPolicy: IfNotPresent
imagePullSecrets: []
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
@ -34,10 +36,14 @@ nodeSelector: {}
tolerations: []
affinity: {}
## Pod annotations
podAnnotations: {}
## Service settings
service:
type: ClusterIP
port: 2021
annotations: {}
## Ingress settings
ingress:
@ -85,16 +91,28 @@ extraVolumeMounts: []
extraVolumes: []
## Resource limits and requests
# resources:
# limits:
# cpu: 500m
# memory: 512Mi
# requests:
# cpu: 100m
# memory: 128Mi
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 500m
# memory: 512Mi
# requests:
# cpu: 100m
# memory: 128Mi
## Application health checks
probes:
startup:
enabled: true
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 15
failureThreshold: 80
successThreshold: 1
path: /health
liveness:
enabled: true
initialDelaySeconds: 30
@ -139,21 +157,18 @@ config:
# Migration options
migration_skip: false # Set to true to skip database migrations
migration_retry: 3 # Number of retries for failed migrations
migration_timeout: "600s" # Timeout for database migrations (default: 10 minutes)
# These are only required for postgres - direct configuration
# These are only required for postgres
host: ""
port: 5432
user: ""
password: ""
name: ""
# Secret configuration for database credentials
existingSecret: "" # Name of existing Kubernetes secret
hostKey: "db-host" # Key in the secret for database host
portKey: "db-port" # Key in the secret for database port
userKey: "db-user" # Key in the secret for database user
passwordKey: "db-password" # Key in the secret for database password
nameKey: "db-name" # Key in the secret for database name
# Secret configuration for postgres credentials
secrets:
existingSecret: "" # Name of existing Kubernetes secret containing postgres credentials
userKey: "username" # Key in the secret for database username
passwordKey: "password" # Key in the secret for database password
# Security settings
# For production, use a generated secret and store in a Kubernetes Secret
@ -208,3 +223,27 @@ config:
user_info_url: ""
redirect_url: ""
name: ""
# Real-time configuration
realtime:
max_connections: 100
ping_interval: "30s"
pong_wait: "60s"
write_wait: "10s"
max_message_size: 512
# Logging configuration
logging:
level: "info"
format: "json"
# Storage configuration
storage:
type: "local"
path: "/donetick-data/uploads"
# Feature flags
features:
notifications: true
realtime: true
oauth: false

View File

@ -1,6 +1,6 @@
apiVersion: v2
name: jellyseerr
description: A Helm chart for Jellyseerr - A fork of Overseerr for Jellyfin support
description: Jellyseerr helm chart for Kubernetes
type: application
version: 0.0.1
appVersion: 2.5.2

View File

@ -4,7 +4,9 @@ A Helm chart for deploying [Jellyseerr](https://github.com/fallenbagel/jellyseer
## 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: https://github.com/rtomik/helm-charts/tree/main/charts/jellyseerr
## Prerequisites
@ -14,104 +16,35 @@ This chart deploys Jellyseerr on a Kubernetes cluster using the Helm package man
## Installing the Chart
To install the chart with the release name `jellyseerr`:
```bash
$ helm repo add rtomik-charts https://rtomik.github.io/helm-charts
$ helm install jellyseerr rtomik-charts/jellyseerr
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install jellyseerr rtomik/jellyseerr
```
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart
To uninstall/delete the `jellyseerr` deployment:
```bash
$ helm uninstall jellyseerr
helm uninstall jellyseerr
```
## Parameters
## Configuration Examples
### Global parameters
### Minimal Installation
| Name | Description | Value |
|------------------------|---------------------------------------------------------------|--------|
| `nameOverride` | String to partially override the release name | `""` |
| `fullnameOverride` | String to fully override the release name | `""` |
```yaml
ingress:
enabled: true
hosts:
- host: jellyseerr.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- jellyseerr.example.com
```
### Image parameters
| 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:
### Custom Timezone and Logging
```yaml
env:
@ -123,20 +56,110 @@ env:
value: "5055"
```
### Using Persistence
By default, persistence is enabled with a 1Gi volume:
```yaml
persistence:
enabled: true
size: 1Gi
```
You can also use an existing PVC:
### Using an Existing PVC
```yaml
persistence:
enabled: true
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

@ -0,0 +1,17 @@
apiVersion: v2
name: joplin-server
description: Joplin Server helm chart for Kubernetes - Note-taking and synchronization server
type: application
version: 0.0.2
appVersion: "3.4.2"
maintainers:
- name: Richard Tomik
email: no@m.com
keywords:
- notes
- synchronization
- joplin
- productivity
home: https://github.com/rtomik/helm-charts
sources:
- https://github.com/laurent22/joplin

View File

@ -0,0 +1,106 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "joplin-server.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "joplin-server.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "joplin-server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "joplin-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
2. Joplin Server is configured with:
- Database: PostgreSQL ({{ .Values.postgresql.external.host }}:{{ .Values.postgresql.external.port }}/{{ .Values.postgresql.external.database }})
- Storage: {{ .Values.joplin.storage.driver }}
{{- if eq .Values.joplin.storage.driver "filesystem" }}
- Data path: {{ .Values.joplin.storage.filesystemPath }}
{{- end }}
- User registration: {{ if .Values.joplin.server.enableUserRegistration }}enabled{{ else }}disabled{{ end }}
- Sharing: {{ if .Values.joplin.server.enableSharing }}enabled{{ else }}disabled{{ end }}
{{- if and (eq .Values.joplin.storage.driver "filesystem") .Values.persistence.enabled }}
3. Data is persisted using PVC: {{ include "joplin-server.fullname" . }}-data
{{- else if eq .Values.joplin.storage.driver "filesystem" }}
3. WARNING: No persistence enabled. Data will be lost when pods are restarted.
{{- else }}
3. Using {{ .Values.joplin.storage.driver }} storage - no local persistence needed.
{{- end }}
{{- if .Values.transcribe.enabled }}
4. AI Transcription service is enabled:
- Transcribe service accessible internally at: {{ include "joplin-server.fullname" . }}-transcribe:{{ .Values.transcribe.service.port }}
{{- if .Values.transcribe.persistence.enabled }}
- Transcribe images stored in PVC: {{ include "joplin-server.fullname" . }}-transcribe-images
{{- end }}
{{- end }}
{{- if .Values.joplin.email.enabled }}
5. Email notifications configured:
- SMTP host: {{ .Values.joplin.email.host }}:{{ .Values.joplin.email.port }}
- From: {{ .Values.joplin.email.fromName }} <{{ .Values.joplin.email.fromEmail }}>
{{- end }}
{{- if not .Values.joplin.admin.email }}
6. IMPORTANT: First-time setup required!
After accessing the application, you'll need to create your first admin user.
Consider setting joplin.admin.email and joplin.admin.password for automated setup.
{{- else }}
6. Admin user configured:
- Email: {{ .Values.joplin.admin.email }}
{{- if .Values.joplin.admin.existingSecret }}
- Password: Retrieved from secret {{ .Values.joplin.admin.existingSecret }}
{{- else }}
- Password: Set in values (consider using existingSecret for production)
{{- end }}
{{- end }}
{{- if not .Values.postgresql.external.enabled }}
7. WARNING: PostgreSQL database not configured!
Please configure postgresql.external settings to connect to your PostgreSQL database.
Joplin Server requires a PostgreSQL database to function.
{{- else if and .Values.postgresql.external.enabled (not .Values.postgresql.external.existingSecret) }}
7. SECURITY NOTE: Database credentials in plain text.
For production use, consider using postgresql.external.existingSecret to store database credentials securely.
{{- end }}
{{- $defaultHost := "joplin.domain.com" }}
{{- $actualHost := "" }}
{{- if .Values.ingress.enabled }}
{{- range .Values.ingress.hosts }}
{{- $actualHost = .host }}
{{- end }}
{{- end }}
{{- if and $actualHost (ne $actualHost $defaultHost) }}
{{- if and .Values.probes.liveness.httpHeaders (len .Values.probes.liveness.httpHeaders) }}
{{- $probeHost := "" }}
{{- range .Values.probes.liveness.httpHeaders }}
{{- if eq .name "Host" }}
{{- $probeHost = .value }}
{{- end }}
{{- end }}
{{- if eq $probeHost $defaultHost }}
8. IMPORTANT: Health check configuration needs updating!
Your ingress host is "{{ $actualHost }}" but health checks are configured for "{{ $defaultHost }}".
Update probes.*.httpHeaders.Host value to match your domain for proper health checks.
{{- end }}
{{- end }}
{{- end }}
For more information about using this Helm chart, please refer to the readme.md file.

View File

@ -0,0 +1,387 @@
# Joplin Server Helm Chart
A Helm chart for deploying [Joplin Server](https://github.com/laurent22/joplin) on Kubernetes.
## Introduction
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: https://github.com/rtomik/helm-charts/tree/main/charts/joplin-server
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- **External PostgreSQL database** (required — Joplin Server does not support SQLite)
- PV provisioner support (if using filesystem storage)
## Installing the Chart
```bash
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install joplin-server rtomik/joplin-server
```
> **Important**: Configure PostgreSQL database settings before installation.
## Uninstalling the Chart
```bash
helm uninstall joplin-server
```
## Configuration Examples
### Minimal Installation
> **Important**: Health check probes require a `Host` header matching your ingress domain. Update `probes.*.httpHeaders` accordingly.
```yaml
postgresql:
external:
enabled: true
host: "postgresql.example.com"
port: 5432
database: "joplin"
user: "joplin"
password: "secure-password"
env:
APP_BASE_URL: "https://joplin.example.com"
probes:
liveness:
httpHeaders:
- name: Host
value: joplin.example.com
readiness:
httpHeaders:
- name: Host
value: joplin.example.com
joplin:
admin:
email: "admin@example.com"
password: "admin-password"
server:
enableUserRegistration: true
ingress:
enabled: true
hosts:
- host: joplin.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- joplin.example.com
secretName: joplin-tls
```
### Production with Existing Secrets
```yaml
postgresql:
external:
enabled: true
host: "postgres-cluster-pooler.dbs.svc.cluster.local"
port: 5432
database: "joplin-server"
existingSecret: "joplin-db-credentials"
userKey: "username"
passwordKey: "password"
joplin:
admin:
existingSecret: "joplin-admin-secret"
emailKey: "email"
passwordKey: "password"
```
### S3 Storage
```yaml
joplin:
storage:
driver: "s3"
s3:
bucket: "joplin-notes"
region: "us-east-1"
existingSecret: "joplin-s3-secret"
accessKeyIdKey: "access-key-id"
secretAccessKeyKey: "secret-access-key"
# No persistence needed when using S3
persistence:
enabled: false
```
### Email Notifications
```yaml
joplin:
email:
enabled: true
host: "smtp.example.com"
port: 587
fromEmail: "joplin@example.com"
fromName: "Joplin Server"
secure: true
existingSecret: "joplin-email-secret"
usernameKey: "username"
passwordKey: "password"
```
### Transcribe Service (AI Transcription)
```yaml
transcribe:
enabled: true
api:
existingSecret: "joplin-transcribe-secret"
keyName: "api-key"
database:
host: "postgresql.example.com"
port: 5432
database: "transcribe"
user: "transcribe"
existingSecret: "transcribe-db-secret"
userKey: "username"
passwordKey: "password"
persistence:
enabled: true
size: 5Gi
```
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
### Image Parameters
| Name | Description | Default |
|------|-------------|---------|
| `image.repository` | Joplin Server image repository | `joplin/server` |
| `image.tag` | Image tag | `3.4.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 | `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
### Health Check Failures / "No Available Server"
Health checks require the correct `Host` header matching your ingress domain:
```yaml
probes:
liveness:
httpHeaders:
- name: Host
value: your-joplin-domain.com
readiness:
httpHeaders:
- name: Host
value: your-joplin-domain.com
```
### Database Connection Issues
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
kubectl logs -f deployment/joplin-server
kubectl describe pod -l app.kubernetes.io/name=joplin-server
```
## Links
- [Joplin GitHub](https://github.com/laurent22/joplin)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/joplin-server)

View File

@ -0,0 +1,45 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "joplin-server.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "joplin-server.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "joplin-server.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "joplin-server.labels" -}}
helm.sh/chart: {{ include "joplin-server.chart" . }}
{{ include "joplin-server.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "joplin-server.selectorLabels" -}}
app.kubernetes.io/name: {{ include "joplin-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@ -0,0 +1,377 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "joplin-server.fullname" . }}
labels:
{{- include "joplin-server.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "joplin-server.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
{{- include "joplin-server.selectorLabels" . | nindent 8 }}
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 22300
protocol: TCP
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
{{- if .Values.probes.liveness.httpHeaders }}
httpHeaders:
{{- toYaml .Values.probes.liveness.httpHeaders | nindent 16 }}
{{- end }}
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
successThreshold: {{ .Values.probes.liveness.successThreshold }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
{{- if .Values.probes.readiness.httpHeaders }}
httpHeaders:
{{- toYaml .Values.probes.readiness.httpHeaders | nindent 16 }}
{{- end }}
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
successThreshold: {{ .Values.probes.readiness.successThreshold }}
{{- end }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- if .Values.postgresql.external.enabled }}
- name: POSTGRES_HOST
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.hostKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.hostKey }}
{{- else }}
value: {{ .Values.postgresql.external.host | quote }}
{{- end }}
- name: POSTGRES_PORT
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.portKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.portKey }}
{{- else }}
value: {{ .Values.postgresql.external.port | quote }}
{{- end }}
- name: POSTGRES_DATABASE
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.databaseKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.databaseKey }}
{{- else }}
value: {{ .Values.postgresql.external.database | quote }}
{{- end }}
- name: POSTGRES_USER
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.userKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.userKey }}
{{- else }}
value: {{ .Values.postgresql.external.user | quote }}
{{- end }}
- name: POSTGRES_PASSWORD
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.passwordKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.passwordKey }}
{{- else }}
value: {{ .Values.postgresql.external.password | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.admin.email }}
- name: ADMIN_EMAIL
{{- if .Values.joplin.admin.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.admin.existingSecret }}
key: {{ .Values.joplin.admin.emailKey }}
{{- else }}
value: {{ .Values.joplin.admin.email | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.admin.password }}
- name: ADMIN_PASSWORD
{{- if .Values.joplin.admin.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.admin.existingSecret }}
key: {{ .Values.joplin.admin.passwordKey }}
{{- else }}
value: {{ .Values.joplin.admin.password | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.server.maxRequestBodySize }}
- name: MAX_REQUEST_BODY_SIZE
value: {{ .Values.joplin.server.maxRequestBodySize | quote }}
{{- end }}
{{- if .Values.joplin.server.sessionTimeout }}
- name: SESSION_TIMEOUT
value: {{ .Values.joplin.server.sessionTimeout | quote }}
{{- end }}
- name: ENABLE_USER_REGISTRATION
value: {{ .Values.joplin.server.enableUserRegistration | quote }}
- name: ENABLE_SHARING
value: {{ .Values.joplin.server.enableSharing | quote }}
- name: ENABLE_PUBLIC_NOTES
value: {{ .Values.joplin.server.enablePublicNotes | quote }}
{{- if eq .Values.joplin.storage.driver "filesystem" }}
- name: STORAGE_CONNECTION_STRING
value: "Type=Filesystem; Path={{ .Values.joplin.storage.filesystemPath }}"
{{- else if eq .Values.joplin.storage.driver "s3" }}
- name: STORAGE_CONNECTION_STRING
value: "Type=S3; Bucket={{ .Values.joplin.storage.s3.bucket }}; Region={{ .Values.joplin.storage.s3.region }}{{- if .Values.joplin.storage.s3.endpoint }}; Endpoint={{ .Values.joplin.storage.s3.endpoint }}{{- end }}"
{{- else }}
- name: STORAGE_CONNECTION_STRING
value: "Type=Database"
{{- end }}
{{- if and (eq .Values.joplin.storage.driver "s3") .Values.joplin.storage.s3.bucket }}
- name: AWS_ACCESS_KEY_ID
{{- if .Values.joplin.storage.s3.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.storage.s3.existingSecret }}
key: {{ .Values.joplin.storage.s3.accessKeyIdKey }}
{{- else }}
value: {{ .Values.joplin.storage.s3.accessKeyId | quote }}
{{- end }}
- name: AWS_SECRET_ACCESS_KEY
{{- if .Values.joplin.storage.s3.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.storage.s3.existingSecret }}
key: {{ .Values.joplin.storage.s3.secretAccessKeyKey }}
{{- else }}
value: {{ .Values.joplin.storage.s3.secretAccessKey | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.email.enabled }}
- name: SMTP_HOST
value: {{ .Values.joplin.email.host | quote }}
- name: SMTP_PORT
value: {{ .Values.joplin.email.port | quote }}
- name: SMTP_USERNAME
{{- if .Values.joplin.email.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.email.existingSecret }}
key: {{ .Values.joplin.email.usernameKey }}
{{- else }}
value: {{ .Values.joplin.email.username | quote }}
{{- end }}
- name: SMTP_PASSWORD
{{- if .Values.joplin.email.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.email.existingSecret }}
key: {{ .Values.joplin.email.passwordKey }}
{{- else }}
value: {{ .Values.joplin.email.password | quote }}
{{- end }}
- name: SMTP_FROM_EMAIL
value: {{ .Values.joplin.email.fromEmail | quote }}
- name: SMTP_FROM_NAME
value: {{ .Values.joplin.email.fromName | quote }}
- name: SMTP_SECURE
value: {{ .Values.joplin.email.secure | quote }}
{{- end }}
{{- if .Values.transcribe.enabled }}
- name: TRANSCRIBE_ENABLED
value: "true"
- name: TRANSCRIBE_API_KEY
{{- if .Values.transcribe.api.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.api.existingSecret }}
key: {{ .Values.transcribe.api.keyName }}
{{- else }}
value: {{ .Values.transcribe.api.key | quote }}
{{- end }}
- name: TRANSCRIBE_BASE_URL
value: "http://{{ include "joplin-server.fullname" . }}-transcribe:{{ .Values.transcribe.service.port }}"
{{- end }}
- name: LOG_LEVEL
value: {{ .Values.joplin.logging.level | quote }}
- name: LOG_TARGET
value: {{ .Values.joplin.logging.target | quote }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
{{- if eq .Values.joplin.storage.driver "filesystem" }}
- name: data
mountPath: {{ .Values.joplin.storage.filesystemPath }}
{{- end }}
{{- if and .Values.security.tls.enabled .Values.security.tls.existingSecret }}
- name: tls-certs
mountPath: /app/certs
readOnly: true
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.transcribe.enabled }}
- name: transcribe
image: "{{ .Values.transcribe.image.repository }}:{{ .Values.transcribe.image.tag }}"
imagePullPolicy: {{ .Values.transcribe.image.pullPolicy }}
ports:
- name: transcribe
containerPort: 4567
protocol: TCP
env:
- name: APP_PORT
value: "4567"
- name: DB_CLIENT
value: "pg"
- name: API_KEY
{{- if .Values.transcribe.api.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.api.existingSecret }}
key: {{ .Values.transcribe.api.keyName }}
{{- else }}
value: {{ .Values.transcribe.api.key | quote }}
{{- end }}
- name: HTR_CLI_IMAGES_FOLDER
value: {{ .Values.transcribe.htr.imagesFolder | quote }}
{{- if .Values.transcribe.database.host }}
- name: QUEUE_DATABASE_HOST
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.hostKey }}
{{- else }}
value: {{ .Values.transcribe.database.host | quote }}
{{- end }}
- name: QUEUE_DATABASE_PORT
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.portKey }}
{{- else }}
value: {{ .Values.transcribe.database.port | quote }}
{{- end }}
- name: QUEUE_DATABASE_NAME
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.databaseKey }}
{{- else }}
value: {{ .Values.transcribe.database.database | quote }}
{{- end }}
- name: QUEUE_DATABASE_USER
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.userKey }}
{{- else }}
value: {{ .Values.transcribe.database.user | quote }}
{{- end }}
- name: QUEUE_DATABASE_PASSWORD
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.passwordKey }}
{{- else }}
value: {{ .Values.transcribe.database.password | quote }}
{{- end }}
{{- end }}
volumeMounts:
- name: transcribe-images
mountPath: {{ .Values.transcribe.htr.imagesFolder }}
- name: docker-socket
mountPath: /var/run/docker.sock
{{- end }}
volumes:
{{- if eq .Values.joplin.storage.driver "filesystem" }}
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "joplin-server.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}
{{- if .Values.transcribe.enabled }}
- name: transcribe-images
{{- if .Values.transcribe.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "joplin-server.fullname" . }}-transcribe-images
{{- else }}
emptyDir: {}
{{- end }}
- name: docker-socket
hostPath:
path: /var/run/docker.sock
type: Socket
{{- end }}
{{- if and .Values.security.tls.enabled .Values.security.tls.existingSecret }}
- name: tls-certs
secret:
secretName: {{ .Values.security.tls.existingSecret }}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "joplin-server.fullname" . }}
labels:
{{- include "joplin-server.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "joplin-server.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,44 @@
{{- if and (eq .Values.joplin.storage.driver "filesystem") .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "joplin-server.fullname" . }}-data
labels:
{{- include "joplin-server.labels" . | nindent 4 }}
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.accessMode | quote }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}
---
{{- if and .Values.transcribe.enabled .Values.transcribe.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "joplin-server.fullname" . }}-transcribe-images
labels:
{{- include "joplin-server.labels" . | nindent 4 }}
app.kubernetes.io/component: transcribe
{{- with .Values.transcribe.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.transcribe.persistence.accessMode | quote }}
{{- if .Values.transcribe.persistence.storageClass }}
storageClassName: {{ .Values.transcribe.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.transcribe.persistence.size | quote }}
{{- end }}

View File

@ -0,0 +1,34 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "joplin-server.fullname" . }}
labels:
{{- include "joplin-server.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "joplin-server.selectorLabels" . | nindent 4 }}
---
{{- if .Values.transcribe.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "joplin-server.fullname" . }}-transcribe
labels:
{{- include "joplin-server.labels" . | nindent 4 }}
app.kubernetes.io/component: transcribe
spec:
type: {{ .Values.transcribe.service.type }}
ports:
- port: {{ .Values.transcribe.service.port }}
targetPort: transcribe
protocol: TCP
name: transcribe
selector:
{{- include "joplin-server.selectorLabels" . | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,268 @@
## Global settings
nameOverride: ""
fullnameOverride: ""
## Image settings
image:
repository: joplin/server
tag: "3.4.2"
pullPolicy: IfNotPresent
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
# Pod security settings
podSecurityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
## Pod scheduling
nodeSelector: {}
tolerations: []
affinity: {}
## Service settings
service:
type: ClusterIP
port: 22300
## Ingress settings
ingress:
enabled: false
className: ""
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: joplin.domain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- joplin.domain.com
## Resource limits and requests
# resources:
# limits:
# cpu: 500m
# memory: 512Mi
# requests:
# cpu: 100m
# memory: 256Mi
## Application health checks
probes:
liveness:
enabled: true
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
successThreshold: 1
path: /api/ping
# Host header for health checks to bypass origin validation
# Update this to match your actual domain
httpHeaders:
- name: Host
value: joplin.domain.com
readiness:
enabled: true
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
path: /api/ping
# Host header for health checks to bypass origin validation
# Update this to match your actual domain
httpHeaders:
- name: Host
value: joplin.domain.com
## Autoscaling configuration
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
## Environment variables
env:
# Application Settings
APP_PORT: "22300"
APP_BASE_URL: "http://localhost:22300"
# Database Settings (PostgreSQL required)
DB_CLIENT: "pg"
# Extra environment variables (for advanced use cases)
extraEnv: []
# Extra volume mounts
extraVolumeMounts: []
# Extra volumes
extraVolumes: []
## PostgreSQL configuration (External database required)
postgresql:
# External PostgreSQL settings (required)
external:
enabled: false
host: ""
port: 5432
database: "joplin"
user: "joplin"
password: ""
# Use existing secret for database credentials
existingSecret: ""
userKey: "username"
passwordKey: "password"
# Optional: only set if host/port/database are also in the secret
hostKey: ""
portKey: ""
databaseKey: ""
## Joplin Server Configuration
joplin:
# Admin settings
admin:
# First admin user email (set during first setup)
email: ""
# First admin user password (set during first setup)
password: ""
# Use existing secret for admin credentials
existingSecret: ""
emailKey: "admin-email"
passwordKey: "admin-password"
# Server settings
server:
# Maximum request body size (in bytes)
maxRequestBodySize: "200mb"
# Session timeout in seconds
sessionTimeout: 86400
# Enable/disable user registration
enableUserRegistration: false
# Enable/disable sharing
enableSharing: true
# Enable/disable public notes
enablePublicNotes: true
# Storage settings
storage:
# Storage driver: database, filesystem, s3, or azure
driver: "database"
# For filesystem storage (requires persistence)
filesystemPath: "/var/lib/joplin"
# For S3 storage (optional)
s3:
bucket: ""
region: ""
accessKeyId: ""
secretAccessKey: ""
endpoint: ""
# Use existing secret for S3 credentials
existingSecret: ""
accessKeyIdKey: "access-key-id"
secretAccessKeyKey: "secret-access-key"
# Email settings (for user registration and notifications)
email:
enabled: false
host: ""
port: 587
username: ""
password: ""
fromEmail: ""
fromName: "Joplin Server"
# Use TLS/SSL
secure: true
# Use existing secret for email credentials
existingSecret: ""
usernameKey: "email-username"
passwordKey: "email-password"
# Logging settings
logging:
level: "info" # error, warn, info, debug
target: "console" # console, file
## Persistence settings (for filesystem storage)
persistence:
enabled: false
storageClass: ""
accessMode: ReadWriteOnce
size: 3Gi
annotations: {}
## Transcribe service (optional AI transcription)
transcribe:
enabled: false
image:
repository: joplin/transcribe
tag: "latest"
pullPolicy: IfNotPresent
# Transcribe API settings
api:
# Shared secret between Joplin Server and Transcribe service
key: ""
# Use existing secret for transcribe API key
existingSecret: ""
keyName: "transcribe-api-key"
# Transcribe service settings
service:
type: ClusterIP
port: 4567
# HTR CLI settings
htr:
# Images folder path
imagesFolder: "/app/images"
# Transcribe persistence (for image storage)
persistence:
enabled: false
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
annotations: {}
# Transcribe database (separate from main Joplin database)
database:
host: ""
port: 5432
database: "transcribe"
user: "transcribe"
password: ""
# Use existing secret for transcribe database credentials
existingSecret: ""
userKey: "username"
passwordKey: "password"
hostKey: "host"
portKey: "port"
databaseKey: "database"
## Security settings
security:
# Enable/disable HTTPS redirect
httpsRedirect: false
# Custom TLS certificate
tls:
enabled: false
# Use existing secret for TLS certificate
existingSecret: ""
certificateKey: "tls.crt"
privateKeyKey: "tls.key"

View File

@ -0,0 +1,16 @@
apiVersion: v2
name: karakeep
description: Karakeep helm chart for Kubernetes
type: application
version: 0.0.1
appVersion: "0.26.0"
maintainers:
- name: Richard Tomik
email: no@m.com
keywords:
- bookmark-manager
- karakeep
- productivity
home: https://github.com/rtomik/helm-charts
sources:
- https://github.com/karakeep-app/karakeep

46
charts/karakeep/NOTES.txt Normal file
View File

@ -0,0 +1,46 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "karakeep.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "karakeep.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "karakeep.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "{{ include "karakeep.selectorLabels" . }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:3000 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:$CONTAINER_PORT
{{- end }}
2. Karakeep has been deployed with the following components:
- Main application (Karakeep)
- MeiliSearch for search functionality
- Chrome browser for web scraping
{{- if not .Values.persistence.enabled }}
3. WARNING: Persistence is disabled. Data will be lost when pods are restarted.
Enable persistence by setting:
persistence.enabled=true
{{- end }}
4. IMPORTANT: Configuration for production:
- Generate a secure 32-character random string for NEXTAUTH_SECRET
- NEXTAUTH_URL is automatically set when ingress is enabled
- Update secrets or environment variables as needed
{{- if not .Values.secrets.create }}
{{- if not .Values.secrets.existingSecret }}
5. Optional: Configure additional API keys via secrets:
- OPENAI_API_KEY for AI features
- MEILI_MASTER_KEY for MeiliSearch authentication
{{- end }}
{{- end }}

201
charts/karakeep/readme.md Normal file
View File

@ -0,0 +1,201 @@
# Karakeep Helm Chart
A Helm chart for deploying [Karakeep](https://github.com/karakeep-app/karakeep), a bookmark management application, on Kubernetes.
## Introduction
This chart deploys Karakeep as a multi-container pod with three services:
1. **Karakeep** — Main bookmark management application
2. **Chrome** — Headless browser for web scraping and preview generation
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
- Kubernetes 1.19+
- Helm 3.2.0+
- PV provisioner support in the underlying infrastructure (if persistence is enabled)
## Installing the Chart
```bash
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install karakeep rtomik/karakeep
```
## Uninstalling the Chart
```bash
helm uninstall karakeep
```
## Configuration Examples
### Minimal Installation
```yaml
ingress:
enabled: true
hosts:
- host: karakeep.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- karakeep.example.com
```
### Production with Secrets
For production, store `NEXTAUTH_SECRET` in a Kubernetes secret. When ingress is enabled, `NEXTAUTH_URL` is automatically set to the ingress hostname.
```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` |
| `revisionHistoryLimit` | Revisions to retain | `3` |
### Pod Security Parameters
| 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.tag` | Karakeep image tag | `0.26.0` |
| `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 Parameters
| Name | Description | Default |
|------|-------------|---------|
| `chrome.image.repository` | Chrome image repository | `gcr.io/zenika-hub/alpine-chrome` |
| `chrome.image.tag` | Chrome image tag | `124` |
| `chrome.image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `chrome.service.port` | Chrome debugging port | `9222` |
### MeiliSearch Parameters
| Name | Description | Default |
|------|-------------|---------|
| `meilisearch.image.repository` | MeiliSearch image repository | `getmeili/meilisearch` |
| `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` |
### Service Parameters
| Name | Description | Default |
|------|-------------|---------|
| `service.type` | Service type | `ClusterIP` |
| `service.port` | Service port | `3000` |
### 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.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.existingSecret` | Use an existing secret | `""` |
| `secrets.env` | Environment variables for the secret | `{}` |
## Troubleshooting
### NEXTAUTH_SECRET Not Set
The default `NEXTAUTH_SECRET` is a placeholder. For production, override it:
```yaml
secrets:
create: true
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

@ -0,0 +1,70 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "karakeep.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "karakeep.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "karakeep.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "karakeep.labels" -}}
helm.sh/chart: {{ include "karakeep.chart" . }}
{{ include "karakeep.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "karakeep.selectorLabels" -}}
app.kubernetes.io/name: {{ include "karakeep.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the secret to use
*/}}
{{- define "karakeep.secretName" -}}
{{- if .Values.secrets.existingSecret }}
{{- .Values.secrets.existingSecret }}
{{- else }}
{{- include "karakeep.fullname" . }}-secret
{{- end }}
{{- end }}
{{/*
Create the name of the data PVC
*/}}
{{- define "karakeep.dataPvcName" -}}
{{- include "karakeep.fullname" . }}-data
{{- end }}
{{/*
Create the name of the meilisearch PVC
*/}}
{{- define "karakeep.meilisearchPvcName" -}}
{{- include "karakeep.fullname" . }}-meilisearch
{{- end }}

View File

@ -0,0 +1,191 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "karakeep.fullname" . }}
labels:
{{- include "karakeep.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "karakeep.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "karakeep.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
# Karakeep main application
- name: karakeep
image: "{{ .Values.karakeep.image.repository }}:{{ .Values.karakeep.image.tag }}"
imagePullPolicy: {{ .Values.karakeep.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.karakeep.service.port }}
protocol: TCP
env:
{{- range .Values.karakeep.env }}
- name: {{ .name }}
{{- if and (eq .name "NEXTAUTH_URL") $.Values.ingress.enabled }}
{{- $host := (index $.Values.ingress.hosts 0).host }}
{{- if $.Values.ingress.tls }}
value: "https://{{ $host }}"
{{- else }}
value: "http://{{ $host }}"
{{- end }}
{{- else }}
value: {{ .value | quote }}
{{- end }}
{{- end }}
{{- range .Values.karakeep.extraEnv }}
- name: {{ .name }}
{{- if .value }}
value: {{ .value | quote }}
{{- else if .valueFrom }}
valueFrom:
{{- toYaml .valueFrom | nindent 16 }}
{{- end }}
{{- end }}
{{- if or .Values.secrets.create .Values.secrets.existingSecret }}
envFrom:
- secretRef:
name: {{ include "karakeep.secretName" . }}
{{- end }}
volumeMounts:
- name: data
mountPath: /data
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
{{- with .Values.karakeep.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 10
# Chrome browser sidecar
- name: chrome
image: "{{ .Values.chrome.image.repository }}:{{ .Values.chrome.image.tag }}"
imagePullPolicy: {{ .Values.chrome.image.pullPolicy }}
args:
{{- range .Values.chrome.args }}
- {{ . | quote }}
{{- end }}
ports:
- name: debug
containerPort: {{ .Values.chrome.service.port }}
protocol: TCP
securityContext:
{{- toYaml .Values.chrome.securityContext | nindent 12 }}
{{- with .Values.chrome.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
livenessProbe:
httpGet:
path: /json/version
port: debug
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /json/version
port: debug
initialDelaySeconds: 10
periodSeconds: 5
# MeiliSearch sidecar
- name: meilisearch
image: "{{ .Values.meilisearch.image.repository }}:{{ .Values.meilisearch.image.tag }}"
imagePullPolicy: {{ .Values.meilisearch.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.meilisearch.service.port }}
protocol: TCP
env:
{{- range .Values.meilisearch.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- range .Values.meilisearch.extraEnv }}
- name: {{ .name }}
{{- if .value }}
value: {{ .value | quote }}
{{- else if .valueFrom }}
valueFrom:
{{- toYaml .valueFrom | nindent 16 }}
{{- end }}
{{- end }}
{{- if or .Values.secrets.create .Values.secrets.existingSecret }}
envFrom:
- secretRef:
name: {{ include "karakeep.secretName" . }}
{{- end }}
volumeMounts:
- name: meilisearch-data
mountPath: /meili_data
securityContext:
{{- toYaml .Values.meilisearch.securityContext | nindent 12 }}
{{- with .Values.meilisearch.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
startupProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 10
volumes:
{{- if .Values.persistence.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ include "karakeep.dataPvcName" . }}
- name: meilisearch-data
persistentVolumeClaim:
claimName: {{ include "karakeep.meilisearchPvcName" . }}
{{- else }}
- name: data
emptyDir: {}
- name: meilisearch-data
emptyDir: {}
{{- end }}

View File

@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "karakeep.fullname" . }}
labels:
{{- include "karakeep.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "karakeep.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,43 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "karakeep.dataPvcName" . }}
labels:
{{- include "karakeep.labels" . | nindent 4 }}
component: data
spec:
accessModes:
- {{ .Values.persistence.data.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.data.size }}
{{- if .Values.persistence.data.storageClass }}
{{- if (eq "-" .Values.persistence.data.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.persistence.data.storageClass }}
{{- end }}
{{- end }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "karakeep.meilisearchPvcName" . }}
labels:
{{- include "karakeep.labels" . | nindent 4 }}
component: meilisearch
spec:
accessModes:
- {{ .Values.persistence.meilisearch.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.meilisearch.size }}
{{- if .Values.persistence.meilisearch.storageClass }}
{{- if (eq "-" .Values.persistence.meilisearch.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.persistence.meilisearch.storageClass }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,13 @@
{{- if .Values.secrets.create }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "karakeep.secretName" . }}
labels:
{{- include "karakeep.labels" . | nindent 4 }}
type: Opaque
data:
{{- range $key, $value := .Values.secrets.env }}
{{ $key }}: {{ $value | b64enc }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "karakeep.fullname" . }}
labels:
{{- include "karakeep.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "karakeep.selectorLabels" . | nindent 4 }}

170
charts/karakeep/values.yaml Normal file
View File

@ -0,0 +1,170 @@
## Global settings
nameOverride: ""
fullnameOverride: ""
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
# Pod security settings
podSecurityContext:
runAsNonRoot: false
runAsUser: 0
fsGroup: 0
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
## Pod scheduling
nodeSelector: {}
tolerations: []
affinity: {}
## Karakeep Web Application
karakeep:
image:
repository: ghcr.io/karakeep-app/karakeep
tag: "0.26.0"
pullPolicy: IfNotPresent
securityContext: {}
env:
- name: DATA_DIR
value: "/data"
- name: MEILI_ADDR
value: "http://localhost:7700"
- name: BROWSER_WEB_URL
value: "http://localhost:9222"
- name: NEXTAUTH_SECRET
value: "changeme-generate-a-secure-random-string"
- name: NEXTAUTH_URL
value: "http://localhost:3000"
extraEnv: []
# - name: OPENAI_API_KEY
# valueFrom:
# secretKeyRef:
# name: karakeep-secrets
# key: openai-api-key
service:
port: 3000
#resources:
# limits:
# cpu: 500m
# memory: 1Gi
# requests:
# cpu: 100m
# memory: 256Mi
## Chrome Browser Service
chrome:
image:
repository: gcr.io/zenika-hub/alpine-chrome
tag: "124"
pullPolicy: IfNotPresent
securityContext: {}
args:
- --no-sandbox
- --disable-gpu
- --disable-dev-shm-usage
- --remote-debugging-address=0.0.0.0
- --remote-debugging-port=9222
- --hide-scrollbars
service:
port: 9222
#resources:
# limits:
# cpu: 500m
# memory: 512Mi
# requests:
# cpu: 100m
# memory: 128Mi
## MeiliSearch Service
meilisearch:
image:
repository: getmeili/meilisearch
tag: "v1.13.3"
pullPolicy: IfNotPresent
securityContext: {}
env:
- name: MEILI_NO_ANALYTICS
value: "true"
- name: MEILI_MAX_INDEXING_MEMORY
value: "512MiB"
- name: MEILI_MAX_INDEXING_THREADS
value: "2"
extraEnv: []
service:
port: 7700
resources:
limits:
cpu: 500m
memory: 1Gi
requests:
cpu: 100m
memory: 256Mi
## Service settings
service:
type: ClusterIP
port: 3000
## Ingress settings
ingress:
enabled: false
className: ""
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: karakeep.<domain.com>
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- karakeep.<domain.com>
## Persistence settings
persistence:
enabled: true
# Karakeep data storage
data:
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
# MeiliSearch data storage
meilisearch:
storageClass: ""
accessMode: ReadWriteOnce
size: 2Gi
## Secret configuration
secrets:
# Set to true to create a secret for environment variables
create: false
# Name of existing secret to use
existingSecret: ""
# Environment variables to include in secret
env: {}
# NEXTAUTH_SECRET: "your-secure-random-string"
# OPENAI_API_KEY: "your-openai-api-key"
# MEILI_MASTER_KEY: "your-meilisearch-master-key"

17
charts/mealie/Chart.yaml Normal file
View File

@ -0,0 +1,17 @@
apiVersion: v2
name: mealie
description: Mealie helm chart for Kubernetes - Recipe management and meal planning
type: application
version: 0.0.2
appVersion: "v3.2.1"
maintainers:
- name: Richard Tomik
email: no@m.com
keywords:
- recipe-management
- meal-planning
- cooking
- mealie
home: https://github.com/rtomik/helm-charts
sources:
- https://github.com/mealie-recipes/mealie

84
charts/mealie/NOTES.txt Normal file
View File

@ -0,0 +1,84 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mealie.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mealie.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mealie.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mealie.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
2. Mealie application is configured with:
- Database: {{ .Values.env.DB_ENGINE }}
- User signup: {{ if eq .Values.env.ALLOW_SIGNUP "true" }}enabled{{ else }}disabled{{ end }}
- API port: {{ .Values.env.API_PORT }}
{{- if .Values.persistence.enabled }}
3. Data is persisted using PVC: {{ include "mealie.fullname" . }}-data
{{- else }}
3. WARNING: No persistence enabled. Data will be lost when pods are restarted.
{{- end }}
{{- if .Values.postgresql.external.enabled }}
4. Using external PostgreSQL database:
- Host: {{ .Values.postgresql.external.host }}
- Database: {{ .Values.postgresql.external.database }}
{{- end }}
{{- if or .Values.email.enabled .Values.ldap.enabled .Values.oidc.enabled .Values.openai.enabled }}
5. Additional features enabled:
{{- if .Values.email.enabled }}
- SMTP Email notifications configured
{{- end }}
{{- if .Values.ldap.enabled }}
- LDAP authentication enabled
{{- end }}
{{- if .Values.oidc.enabled }}
- OpenID Connect (OIDC) authentication enabled
{{- end }}
{{- if .Values.openai.enabled }}
- OpenAI integration enabled for AI features
{{- end }}
{{- end }}
{{- if or .Values.postgresql.external.existingSecret .Values.email.existingSecret .Values.ldap.existingSecret .Values.oidc.existingSecret .Values.openai.existingSecret .Values.tls.existingSecret }}
6. Using external secrets for sensitive information:
{{- if .Values.postgresql.external.existingSecret }}
- Database credentials from: {{ .Values.postgresql.external.existingSecret }}
{{- end }}
{{- if .Values.email.existingSecret }}
- SMTP credentials from: {{ .Values.email.existingSecret }}
{{- end }}
{{- if .Values.ldap.existingSecret }}
- LDAP credentials from: {{ .Values.ldap.existingSecret }}
{{- end }}
{{- if .Values.oidc.existingSecret }}
- OIDC credentials from: {{ .Values.oidc.existingSecret }}
{{- end }}
{{- if .Values.openai.existingSecret }}
- OpenAI API key from: {{ .Values.openai.existingSecret }}
{{- end }}
{{- if .Values.tls.existingSecret }}
- TLS certificates from: {{ .Values.tls.existingSecret }}
{{- end }}
{{- else }}
6. SECURITY NOTE: For production use, it's recommended to store sensitive data in Kubernetes Secrets.
Consider using existingSecret options for database, email, LDAP, OIDC, and OpenAI configurations.
{{- end }}
For more information about using this Helm chart, please refer to the readme.md file.

366
charts/mealie/readme.md Normal file
View File

@ -0,0 +1,366 @@
# Mealie Helm Chart
A Helm chart for deploying [Mealie](https://github.com/mealie-recipes/mealie), a recipe management and meal planning application, on Kubernetes.
## Introduction
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: https://github.com/rtomik/helm-charts/tree/main/charts/mealie
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- External PostgreSQL database (recommended, e.g. [CloudNativePG](https://cloudnative-pg.io/))
- PV provisioner support (if persistence is needed)
## Installing the Chart
```bash
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install mealie rtomik/mealie
```
## Uninstalling the Chart
```bash
helm uninstall mealie
```
## Configuration Examples
### Minimal Installation
```yaml
persistence:
enabled: true
size: 10Gi
ingress:
enabled: true
hosts:
- host: mealie.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- mealie.example.com
secretName: mealie-tls
```
### PostgreSQL Configuration
```yaml
postgresql:
external:
enabled: true
host: "postgresql.example.com"
port: 5432
database: "mealie"
user: "mealie"
existingSecret: "mealie-postgresql-secret"
userKey: "username"
passwordKey: "password"
env:
DB_ENGINE: "postgres"
```
### OIDC Authentication
```yaml
oidc:
enabled: true
configurationUrl: "https://auth.example.com/.well-known/openid-configuration"
clientId: "mealie-client"
existingSecret: "mealie-oidc-secret"
clientIdKey: "client-id"
clientSecretKey: "client-secret"
autoRedirect: true
providerName: "CompanySSO"
```
### OpenAI Integration
```yaml
openai:
enabled: true
baseUrl: "https://api.openai.com/v1"
existingSecret: "mealie-openai-secret"
apiKeyKey: "api-key"
model: "gpt-4"
enableImageServices: true
```
### LDAP Authentication
```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"
```
### Email Configuration
```yaml
email:
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
- **Database connection issues**: Verify credentials and network connectivity
- **Persistence issues**: Check StorageClass and PVC configuration
- **Authentication problems**: Verify LDAP/OIDC configuration and network access
- **Performance issues**: Adjust resource limits and consider using an external database
```bash
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

@ -0,0 +1,45 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "mealie.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "mealie.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "mealie.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "mealie.labels" -}}
helm.sh/chart: {{ include "mealie.chart" . }}
{{ include "mealie.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "mealie.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mealie.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@ -0,0 +1,337 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mealie.fullname" . }}
labels:
{{- include "mealie.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "mealie.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
{{- include "mealie.selectorLabels" . | nindent 8 }}
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 9000
protocol: TCP
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
successThreshold: {{ .Values.probes.liveness.successThreshold }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
successThreshold: {{ .Values.probes.readiness.successThreshold }}
{{- end }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- if .Values.postgresql.external.enabled }}
- name: DB_ENGINE
value: "postgres"
- name: POSTGRES_SERVER
value: {{ .Values.postgresql.external.host | quote }}
- name: POSTGRES_PORT
value: {{ .Values.postgresql.external.port | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgresql.external.database | quote }}
- name: POSTGRES_USER
{{- if .Values.postgresql.external.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.userKey }}
{{- else }}
value: {{ .Values.postgresql.external.user | quote }}
{{- end }}
- name: POSTGRES_PASSWORD
{{- if .Values.postgresql.external.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.passwordKey }}
{{- else }}
value: {{ .Values.postgresql.external.password | quote }}
{{- end }}
{{- end }}
{{- if .Values.email.enabled }}
- name: SMTP_HOST
{{- if .Values.email.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.email.existingSecret }}
key: "smtp-host"
{{- else }}
value: {{ .Values.email.host | quote }}
{{- end }}
- name: SMTP_PORT
value: {{ .Values.email.port | quote }}
- name: SMTP_FROM_NAME
value: {{ .Values.email.fromName | quote }}
- name: SMTP_AUTH_STRATEGY
value: {{ .Values.email.authStrategy | quote }}
- name: SMTP_FROM_EMAIL
value: {{ .Values.email.fromEmail | quote }}
{{- if and .Values.email.user (or (eq .Values.email.authStrategy "TLS") (eq .Values.email.authStrategy "SSL")) }}
- name: SMTP_USER
{{- if .Values.email.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.email.existingSecret }}
key: {{ .Values.email.userKey }}
{{- else }}
value: {{ .Values.email.user | quote }}
{{- end }}
{{- end }}
{{- if and .Values.email.password (or (eq .Values.email.authStrategy "TLS") (eq .Values.email.authStrategy "SSL")) }}
- name: SMTP_PASSWORD
{{- if .Values.email.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.email.existingSecret }}
key: {{ .Values.email.passwordKey }}
{{- else }}
value: {{ .Values.email.password | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.ldap.enabled }}
- name: LDAP_AUTH_ENABLED
value: "true"
- name: LDAP_SERVER_URL
value: {{ .Values.ldap.serverUrl | quote }}
- name: LDAP_TLS_INSECURE
value: {{ .Values.ldap.tlsInsecure | quote }}
{{- if .Values.ldap.tlsCaCertFile }}
- name: LDAP_TLS_CACERTFILE
value: {{ .Values.ldap.tlsCaCertFile | quote }}
{{- end }}
- name: LDAP_ENABLE_STARTTLS
value: {{ .Values.ldap.enableStartTls | quote }}
- name: LDAP_BASE_DN
value: {{ .Values.ldap.baseDn | quote }}
{{- if .Values.ldap.queryBind }}
- name: LDAP_QUERY_BIND
value: {{ .Values.ldap.queryBind | quote }}
{{- end }}
{{- if .Values.ldap.queryPassword }}
- name: LDAP_QUERY_PASSWORD
{{- if .Values.ldap.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.ldap.existingSecret }}
key: {{ .Values.ldap.passwordKey }}
{{- else }}
value: {{ .Values.ldap.queryPassword | quote }}
{{- end }}
{{- end }}
{{- if .Values.ldap.userFilter }}
- name: LDAP_USER_FILTER
value: {{ .Values.ldap.userFilter | quote }}
{{- end }}
{{- if .Values.ldap.adminFilter }}
- name: LDAP_ADMIN_FILTER
value: {{ .Values.ldap.adminFilter | quote }}
{{- end }}
- name: LDAP_ID_ATTRIBUTE
value: {{ .Values.ldap.idAttribute | quote }}
- name: LDAP_NAME_ATTRIBUTE
value: {{ .Values.ldap.nameAttribute | quote }}
- name: LDAP_MAIL_ATTRIBUTE
value: {{ .Values.ldap.mailAttribute | quote }}
{{- end }}
{{- if .Values.oidc.enabled }}
- name: OIDC_AUTH_ENABLED
value: "true"
- name: OIDC_SIGNUP_ENABLED
value: {{ .Values.oidc.signupEnabled | quote }}
- name: OIDC_CONFIGURATION_URL
value: {{ .Values.oidc.configurationUrl | quote }}
- name: OIDC_CLIENT_ID
{{- if .Values.oidc.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.oidc.existingSecret }}
key: {{ .Values.oidc.clientIdKey }}
{{- else }}
value: {{ .Values.oidc.clientId | quote }}
{{- end }}
- name: OIDC_CLIENT_SECRET
{{- if .Values.oidc.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.oidc.existingSecret }}
key: {{ .Values.oidc.clientSecretKey }}
{{- else }}
value: {{ .Values.oidc.clientSecret | quote }}
{{- end }}
{{- if .Values.oidc.userGroup }}
- name: OIDC_USER_GROUP
value: {{ .Values.oidc.userGroup | quote }}
{{- end }}
{{- if .Values.oidc.adminGroup }}
- name: OIDC_ADMIN_GROUP
value: {{ .Values.oidc.adminGroup | quote }}
{{- end }}
- name: OIDC_AUTO_REDIRECT
value: {{ .Values.oidc.autoRedirect | quote }}
- name: OIDC_PROVIDER_NAME
value: {{ .Values.oidc.providerName | quote }}
- name: OIDC_REMEMBER_ME
value: {{ .Values.oidc.rememberMe | quote }}
- name: OIDC_SIGNING_ALGORITHM
value: {{ .Values.oidc.signingAlgorithm | quote }}
- name: OIDC_USER_CLAIM
value: {{ .Values.oidc.userClaim | quote }}
- name: OIDC_NAME_CLAIM
value: {{ .Values.oidc.nameClaim | quote }}
- name: OIDC_GROUPS_CLAIM
value: {{ .Values.oidc.groupsClaim | quote }}
{{- if .Values.oidc.scopesOverride }}
- name: OIDC_SCOPES_OVERRIDE
value: {{ .Values.oidc.scopesOverride | quote }}
{{- end }}
{{- if .Values.oidc.tlsCaCertFile }}
- name: OIDC_TLS_CACERTFILE
value: {{ .Values.oidc.tlsCaCertFile | quote }}
{{- end }}
{{- end }}
{{- if .Values.openai.enabled }}
{{- if .Values.openai.baseUrl }}
- name: OPENAI_BASE_URL
value: {{ .Values.openai.baseUrl | quote }}
{{- end }}
- name: OPENAI_API_KEY
{{- if .Values.openai.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.openai.existingSecret }}
key: {{ .Values.openai.apiKeyKey }}
{{- else }}
value: {{ .Values.openai.apiKey | quote }}
{{- end }}
- name: OPENAI_MODEL
value: {{ .Values.openai.model | quote }}
{{- if .Values.openai.customHeaders }}
- name: OPENAI_CUSTOM_HEADERS
value: {{ .Values.openai.customHeaders | quote }}
{{- end }}
{{- if .Values.openai.customParams }}
- name: OPENAI_CUSTOM_PARAMS
value: {{ .Values.openai.customParams | quote }}
{{- end }}
- name: OPENAI_ENABLE_IMAGE_SERVICES
value: {{ .Values.openai.enableImageServices | quote }}
- name: OPENAI_WORKERS
value: {{ .Values.openai.workers | quote }}
- name: OPENAI_SEND_DATABASE_DATA
value: {{ .Values.openai.sendDatabaseData | quote }}
- name: OPENAI_REQUEST_TIMEOUT
value: {{ .Values.openai.requestTimeout | quote }}
{{- end }}
{{- if .Values.tls.enabled }}
{{- if .Values.tls.existingSecret }}
- name: TLS_CERTIFICATE_PATH
value: "/app/certs/{{ .Values.tls.certificateKey }}"
- name: TLS_PRIVATE_KEY_PATH
value: "/app/certs/{{ .Values.tls.privateKeyKey }}"
{{- else }}
- name: TLS_CERTIFICATE_PATH
value: {{ .Values.tls.certificatePath | quote }}
- name: TLS_PRIVATE_KEY_PATH
value: {{ .Values.tls.privateKeyPath | quote }}
{{- end }}
{{- end }}
{{- range $key, $value := .Values.theme.light }}
- name: THEME_LIGHT_{{ $key | upper }}
value: {{ $value | quote }}
{{- end }}
{{- range $key, $value := .Values.theme.dark }}
- name: THEME_DARK_{{ $key | upper }}
value: {{ $value | quote }}
{{- end }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /app/data
{{- if and .Values.tls.enabled .Values.tls.existingSecret }}
- name: tls-certs
mountPath: /app/certs
readOnly: true
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "mealie.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
{{- if and .Values.tls.enabled .Values.tls.existingSecret }}
- name: tls-certs
secret:
secretName: {{ .Values.tls.existingSecret }}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mealie.fullname" . }}
labels:
{{- include "mealie.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "mealie.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,27 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "mealie.fullname" . }}-data
labels:
{{- include "mealie.labels" . | nindent 4 }}
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.accessMode | quote }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- if .Values.persistence.selector }}
{{- with .Values.persistence.selector }}
selector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "mealie.fullname" . }}
labels:
{{- include "mealie.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "mealie.selectorLabels" . | nindent 4 }}

259
charts/mealie/values.yaml Normal file
View File

@ -0,0 +1,259 @@
## Global settings
nameOverride: ""
fullnameOverride: ""
## Image settings
image:
repository: ghcr.io/mealie-recipes/mealie
tag: "v3.2.1"
pullPolicy: IfNotPresent
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
# Pod security settings
podSecurityContext:
runAsNonRoot: false
runAsUser: 911
fsGroup: 911
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
## Pod scheduling
nodeSelector: {}
tolerations: []
affinity: {}
## Service settings
service:
type: ClusterIP
port: 9000
## Ingress settings
ingress:
enabled: false
className: ""
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: mealie.domain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- mealie.domain.com
## Persistence settings
persistence:
enabled: false
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
annotations: {}
## Resource limits and requests
# resources:
# limits:
# cpu: 1000m
# memory: 1000Mi
# requests:
# cpu: 100m
# memory: 256Mi
## Application health checks
probes:
liveness:
enabled: true
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
successThreshold: 1
path: /
readiness:
enabled: true
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
path: /
## Autoscaling configuration
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
## Environment variables
env:
# General Settings
PUID: "911"
PGID: "911"
DEFAULT_GROUP: "Home"
DEFAULT_HOUSEHOLD: "Family"
BASE_URL: "http://localhost:9000"
TOKEN_TIME: "48"
API_PORT: "9000"
API_DOCS: "true"
TZ: "UTC"
ALLOW_SIGNUP: "false"
ALLOW_PASSWORD_LOGIN: "true"
LOG_LEVEL: "info"
DAILY_SCHEDULE_TIME: "23:45"
# Security
SECURITY_MAX_LOGIN_ATTEMPTS: "5"
SECURITY_USER_LOCKOUT_TIME: "24"
# Database
DB_ENGINE: "postgres" # postgres or sqlite
# Webworker
UVICORN_WORKERS: "1"
# Extra environment variables (for advanced use cases)
extraEnv: []
# - name: POSTGRES_USER
# value: "mealie"
# - name: POSTGRES_PASSWORD
# value: "mealie"
# - name: POSTGRES_SERVER
# value: "postgres"
# - name: POSTGRES_PORT
# value: "5432"
# - name: POSTGRES_DB
# value: "mealie"
# Extra volume mounts
extraVolumeMounts: []
# Extra volumes
extraVolumes: []
## PostgreSQL configuration (when using external database)
postgresql:
enabled: false
# External PostgreSQL settings
external:
enabled: false
host: ""
port: 5432
database: "mealie"
user: "mealie"
password: ""
# Use existing secret for database credentials
existingSecret: ""
userKey: "username"
passwordKey: "password"
## SMTP Email configuration
email:
enabled: false
host: ""
port: 587
fromName: "Mealie"
authStrategy: "TLS" # TLS, SSL, NONE
fromEmail: ""
user: ""
password: ""
# Use existing secret for SMTP credentials
existingSecret: ""
userKey: "smtp-user"
passwordKey: "smtp-password"
## LDAP Authentication
ldap:
enabled: false
serverUrl: ""
tlsInsecure: false
tlsCaCertFile: ""
enableStartTls: false
baseDn: ""
queryBind: ""
queryPassword: ""
userFilter: ""
adminFilter: ""
idAttribute: "uid"
nameAttribute: "name"
mailAttribute: "mail"
# Use existing secret for LDAP credentials
existingSecret: ""
passwordKey: "ldap-password"
## OpenID Connect (OIDC)
oidc:
enabled: false
signupEnabled: true
configurationUrl: ""
clientId: ""
clientSecret: ""
userGroup: ""
adminGroup: ""
autoRedirect: false
providerName: "OAuth"
rememberMe: false
signingAlgorithm: "RS256"
userClaim: "email"
nameClaim: "name"
groupsClaim: "groups"
scopesOverride: ""
tlsCaCertFile: ""
# Use existing secret for OIDC credentials
existingSecret: ""
clientIdKey: "oidc-client-id"
clientSecretKey: "oidc-client-secret"
## OpenAI Integration
openai:
enabled: false
baseUrl: ""
apiKey: ""
model: "gpt-4o"
customHeaders: ""
customParams: ""
enableImageServices: true
workers: 2
sendDatabaseData: true
requestTimeout: 60
# Use existing secret for OpenAI API key
existingSecret: ""
apiKeyKey: "openai-api-key"
## TLS Configuration
tls:
enabled: false
certificatePath: ""
privateKeyPath: ""
# Use existing secret for TLS certificates
existingSecret: ""
certificateKey: "tls.crt"
privateKeyKey: "tls.key"
## Theming
theme:
light:
primary: "#E58325"
accent: "#007A99"
secondary: "#973542"
success: "#43A047"
info: "#1976D2"
warning: "#FF6D00"
error: "#EF5350"
dark:
primary: "#E58325"
accent: "#007A99"
secondary: "#973542"
success: "#43A047"
info: "#1976D2"
warning: "#FF6D00"
error: "#EF5350"

17
charts/norish/Chart.yaml Normal file
View File

@ -0,0 +1,17 @@
apiVersion: v2
name: norish
description: Norish helm chart for Kubernetes - A recipe management and meal planning application
type: application
version: 0.0.5
appVersion: "v0.15.4-beta"
maintainers:
- name: Richard Tomik
email: no@m.com
keywords:
- recipe
- meal-planning
- food
- norish
home: https://github.com/rtomik/helm-charts
sources:
- https://github.com/norishapp/norish

355
charts/norish/readme.md Normal file
View File

@ -0,0 +1,355 @@
# Norish Helm Chart
A Helm chart for deploying [Norish](https://github.com/norishapp/norish), a recipe management and meal planning application, on Kubernetes.
## Introduction
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.
Source code: https://github.com/rtomik/helm-charts/tree/main/charts/norish
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- **PostgreSQL database** (required)
- **Redis server** (required)
- PV provisioner support (if persistence is enabled)
## Installing the Chart
```bash
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install norish rtomik/norish
```
## Uninstalling the Chart
```bash
helm uninstall norish
```
## Configuration Examples
### Minimal Installation (Password Authentication)
```yaml
database:
host: "postgresql.default.svc.cluster.local"
port: 5432
name: norish
username: norish
password: "secure-password"
redis:
host: "redis.default.svc.cluster.local"
port: 6379
database: 0
config:
authUrl: "https://norish.example.com"
masterKey:
value: "<your-32-byte-base64-key>" # Generate: openssl rand -base64 32
ingress:
enabled: true
hosts:
- host: norish.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- norish.example.com
```
### Production with Existing Secrets
```yaml
database:
host: "postgresql.default.svc.cluster.local"
existingSecret: "norish-db-secret"
usernameKey: "username"
passwordKey: "password"
redis:
existingSecret: "norish-redis-secret"
urlKey: "redis-url"
config:
authUrl: "https://norish.example.com"
masterKey:
existingSecret: "norish-master-key"
secretKey: "master-key"
```
Create the required secrets:
```bash
kubectl create secret generic norish-db-secret \
--from-literal=username="norish" \
--from-literal=password="secure-db-password"
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 \
--from-literal=master-key="$(openssl rand -base64 32)"
```
### OIDC Authentication
```yaml
config:
auth:
oidc:
enabled: true
name: "Authentik"
issuer: "https://auth.example.com/application/o/norish/"
clientId: "<your-client-id>"
clientSecret: "<your-client-secret>"
# Optional: allow password auth alongside OIDC
passwordAuthEnabled: "true"
```
### GitHub OAuth
1. Create a GitHub OAuth App at https://github.com/settings/developers
2. Set Authorization callback URL to: `https://norish.example.com/api/auth/callback/github`
```yaml
config:
auth:
github:
enabled: true
clientId: "<your-github-client-id>"
clientSecret: "<your-github-client-secret>"
```
### Google OAuth
1. Create OAuth credentials at https://console.cloud.google.com/apis/credentials
2. Set Authorized redirect URI to: `https://norish.example.com/api/auth/callback/google`
```yaml
config:
auth:
google:
enabled: true
clientId: "<your-google-client-id>"
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
- **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
kubectl get pods -l app.kubernetes.io/name=norish
kubectl logs -l app.kubernetes.io/name=norish
```
## Links
- [Norish GitHub](https://github.com/norishapp/norish)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/norish)

View File

@ -0,0 +1,74 @@
Thank you for installing {{ .Chart.Name }}!
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }} -n {{ .Release.Namespace }}
$ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }}
{{- if .Values.ingress.enabled }}
Application URL:
{{- range .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}{{ range .paths }}{{ .path }}{{ end }}
{{- end }}
{{- else }}
Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "norish.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "norish.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "norish.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "norish.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
{{- end }}
IMPORTANT CONFIGURATION NOTES:
1. Database Configuration:
{{- if .Values.database.host }}
Using external PostgreSQL at: {{ .Values.database.host }}:{{ .Values.database.port }}
{{- else }}
⚠️ WARNING: Database host is not configured!
Configure database.host to point to your PostgreSQL server.
{{- end }}
2. Master Key:
{{- if .Values.config.masterKey.existingSecret }}
Using existing secret: {{ .Values.config.masterKey.existingSecret }}
{{- else }}
{{- if not .Values.config.masterKey.value }}
⚠️ WARNING: Master key is not set! Generate one with: openssl rand -base64 32
{{- else }}
Master key configured from values.yaml
{{- end }}
{{- end }}
3. Authentication:
{{- if or .Values.config.auth.oidc.enabled .Values.config.auth.github.enabled .Values.config.auth.google.enabled }}
{{- if .Values.config.auth.oidc.enabled }}
- OIDC provider: {{ .Values.config.auth.oidc.name }}
{{- end }}
{{- if .Values.config.auth.github.enabled }}
- GitHub OAuth enabled
{{- end }}
{{- if .Values.config.auth.google.enabled }}
- Google OAuth enabled
{{- end }}
After first login, configure additional providers in Settings → Admin
{{- else }}
⚠️ WARNING: No authentication provider configured!
Configure ONE provider (OIDC, GitHub, or Google) to create your admin account.
{{- end }}
For more information, visit: https://github.com/norishapp/norish

View File

@ -0,0 +1,97 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "norish.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "norish.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "norish.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "norish.labels" -}}
helm.sh/chart: {{ include "norish.chart" . }}
{{ include "norish.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "norish.selectorLabels" -}}
app.kubernetes.io/name: {{ include "norish.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Database connection URL
*/}}
{{- define "norish.databaseUrl" -}}
{{- $username := .Values.database.username }}
{{- $password := .Values.database.password }}
{{- $host := .Values.database.host }}
{{- $port := .Values.database.port }}
{{- $database := .Values.database.name }}
{{- printf "postgres://%s:%s@%s:%d/%s" $username $password $host (int $port) $database }}
{{- 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

@ -0,0 +1,292 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "norish.fullname" . }}
labels:
{{- include "norish.labels" . | nindent 4 }}
app.kubernetes.io/component: app
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "norish.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
{{- include "norish.selectorLabels" . | nindent 8 }}
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- if .Values.probes.startup.enabled }}
startupProbe:
httpGet:
path: {{ .Values.probes.startup.path }}
port: http
initialDelaySeconds: {{ .Values.probes.startup.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.startup.periodSeconds }}
timeoutSeconds: {{ .Values.probes.startup.timeoutSeconds }}
failureThreshold: {{ .Values.probes.startup.failureThreshold }}
successThreshold: {{ .Values.probes.startup.successThreshold }}
{{- end }}
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
successThreshold: {{ .Values.probes.liveness.successThreshold }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
successThreshold: {{ .Values.probes.readiness.successThreshold }}
{{- end }}
env:
- name: AUTH_URL
value: {{ .Values.config.authUrl | quote }}
{{- if .Values.chrome.enabled }}
- name: CHROME_WS_ENDPOINT
value: "ws://localhost:{{ .Values.chrome.port }}"
{{- end }}
{{- if .Values.config.logLevel }}
- name: NEXT_PUBLIC_LOG_LEVEL
value: {{ .Values.config.logLevel | quote }}
{{- end }}
{{- if .Values.config.trustedOrigins }}
- name: TRUSTED_ORIGINS
value: {{ .Values.config.trustedOrigins | quote }}
{{- end }}
{{- if .Values.config.passwordAuthEnabled }}
- name: PASSWORD_AUTH_ENABLED
value: {{ .Values.config.passwordAuthEnabled | quote }}
{{- end }}
{{- if .Values.database.existingSecret }}
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: {{ .Values.database.existingSecret }}
key: {{ .Values.database.usernameKey }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.database.existingSecret }}
key: {{ .Values.database.passwordKey }}
{{- if .Values.database.databaseKey }}
- name: DB_NAME
valueFrom:
secretKeyRef:
name: {{ .Values.database.existingSecret }}
key: {{ .Values.database.databaseKey }}
{{- else }}
- name: DB_NAME
value: {{ .Values.database.name | quote }}
{{- end }}
{{- if .Values.database.hostKey }}
- name: DB_HOST
valueFrom:
secretKeyRef:
name: {{ .Values.database.existingSecret }}
key: {{ .Values.database.hostKey }}
{{- else }}
- name: DB_HOST
value: {{ .Values.database.host | quote }}
{{- end }}
- name: DB_PORT
value: {{ .Values.database.port | quote }}
- name: DATABASE_URL
value: "postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)"
{{- else }}
- name: DATABASE_URL
value: {{ include "norish.databaseUrl" . | quote }}
{{- end }}
- name: MASTER_KEY
valueFrom:
secretKeyRef:
{{- if .Values.config.masterKey.existingSecret }}
name: {{ .Values.config.masterKey.existingSecret }}
key: {{ .Values.config.masterKey.secretKey }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: master-key
{{- 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 }}
- name: OIDC_NAME
value: {{ .Values.config.auth.oidc.name | quote }}
- name: OIDC_ISSUER
value: {{ .Values.config.auth.oidc.issuer | quote }}
{{- if .Values.config.auth.oidc.wellKnown }}
- name: OIDC_WELLKNOWN
value: {{ .Values.config.auth.oidc.wellKnown | quote }}
{{- end }}
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
{{- if .Values.config.auth.oidc.existingSecret }}
name: {{ .Values.config.auth.oidc.existingSecret }}
key: {{ .Values.config.auth.oidc.clientIdKey }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: oidc-client-id
{{- end }}
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
{{- if .Values.config.auth.oidc.existingSecret }}
name: {{ .Values.config.auth.oidc.existingSecret }}
key: {{ .Values.config.auth.oidc.clientSecretKey }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: oidc-client-secret
{{- end }}
{{- end }}
{{- if .Values.config.auth.github.enabled }}
- name: GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
{{- if .Values.config.auth.github.existingSecret }}
name: {{ .Values.config.auth.github.existingSecret }}
key: {{ .Values.config.auth.github.clientIdKey }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: github-client-id
{{- end }}
- name: GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
{{- if .Values.config.auth.github.existingSecret }}
name: {{ .Values.config.auth.github.existingSecret }}
key: {{ .Values.config.auth.github.clientSecretKey }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: github-client-secret
{{- end }}
{{- end }}
{{- if .Values.config.auth.google.enabled }}
- name: GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
{{- if .Values.config.auth.google.existingSecret }}
name: {{ .Values.config.auth.google.existingSecret }}
key: {{ .Values.config.auth.google.clientIdKey }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: google-client-id
{{- end }}
- name: GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
{{- if .Values.config.auth.google.existingSecret }}
name: {{ .Values.config.auth.google.existingSecret }}
key: {{ .Values.config.auth.google.clientSecretKey }}
{{- else }}
name: {{ include "norish.fullname" . }}-secret
key: google-client-secret
{{- end }}
{{- end }}
{{- with .Values.config.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: uploads
mountPath: /app/uploads
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.chrome.enabled }}
- name: chrome-headless
image: "{{ .Values.chrome.image.repository }}:{{ .Values.chrome.image.tag }}"
imagePullPolicy: {{ .Values.chrome.image.pullPolicy }}
securityContext:
{{- toYaml .Values.chrome.securityContext | nindent 12 }}
ports:
- name: chrome
containerPort: {{ .Values.chrome.port }}
protocol: TCP
command:
- chromium-browser
args:
- "--no-sandbox"
- "--disable-gpu"
- "--disable-dev-shm-usage"
- "--remote-debugging-address=0.0.0.0"
- "--remote-debugging-port={{ .Values.chrome.port }}"
- "--headless"
resources:
{{- toYaml .Values.chrome.resources | nindent 12 }}
{{- end }}
volumes:
{{- if .Values.persistence.enabled }}
- name: uploads
persistentVolumeClaim:
{{- if .Values.persistence.existingClaim }}
claimName: {{ .Values.persistence.existingClaim }}
{{- else }}
claimName: {{ include "norish.fullname" . }}-uploads
{{- end }}
{{- else }}
- name: uploads
emptyDir: {}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "norish.fullname" . }}
labels:
{{- include "norish.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "norish.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,22 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "norish.fullname" . }}-uploads
labels:
{{- include "norish.labels" . | nindent 4 }}
app.kubernetes.io/component: app
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}

View File

@ -0,0 +1,39 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "norish.fullname" . }}-secret
labels:
{{- include "norish.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- if not .Values.config.masterKey.existingSecret }}
master-key: {{ .Values.config.masterKey.value | required "config.masterKey.value is required when config.masterKey.existingSecret is not set" | quote }}
{{- end }}
{{- if not .Values.database.existingSecret }}
database-url: {{ include "norish.databaseUrl" . | quote }}
{{- 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 not .Values.config.auth.oidc.existingSecret }}
oidc-client-id: {{ .Values.config.auth.oidc.clientId | quote }}
oidc-client-secret: {{ .Values.config.auth.oidc.clientSecret | quote }}
{{- end }}
{{- end }}
{{- if .Values.config.auth.github.enabled }}
{{- if not .Values.config.auth.github.existingSecret }}
github-client-id: {{ .Values.config.auth.github.clientId | quote }}
github-client-secret: {{ .Values.config.auth.github.clientSecret | quote }}
{{- end }}
{{- end }}
{{- if .Values.config.auth.google.enabled }}
{{- if not .Values.config.auth.google.existingSecret }}
google-client-id: {{ .Values.config.auth.google.clientId | quote }}
google-client-secret: {{ .Values.config.auth.google.clientSecret | quote }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "norish.fullname" . }}
labels:
{{- include "norish.labels" . | nindent 4 }}
app.kubernetes.io/component: app
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "norish.selectorLabels" . | nindent 4 }}

259
charts/norish/values.yaml Normal file
View File

@ -0,0 +1,259 @@
## Global settings
nameOverride: ""
fullnameOverride: ""
## Image settings
image:
repository: norishapp/norish
tag: "v0.16.2-beta"
pullPolicy: IfNotPresent
imagePullSecrets: []
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
# Pod security settings
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
## Pod scheduling
nodeSelector: {}
tolerations: []
affinity: {}
## Pod annotations
podAnnotations: {}
## Service settings
service:
type: ClusterIP
port: 3000
annotations: {}
## Ingress settings
ingress:
enabled: false
className: ""
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: norish.domain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- norish.domain.com
# Optional: specify the name of an existing TLS secret
# secretName: "existing-tls-secret"
## Persistence settings
persistence:
enabled: true
# Use an existing PVC instead of creating a new one
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
annotations: {}
# Extra volume mounts
extraVolumeMounts: []
# Extra volumes
extraVolumes: []
## Resource limits and requests
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 500m
# memory: 512Mi
# requests:
# cpu: 100m
# memory: 128Mi
## Application health checks
probes:
startup:
enabled: true
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
successThreshold: 1
path: /
liveness:
enabled: true
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
path: /
readiness:
enabled: true
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
path: /
## Application configuration
config:
# Application URL (required)
# This should match your ingress hostname
authUrl: "http://norish.domain.com"
# Extra environment variables
# Example:
# extraEnv:
# - name: MY_CUSTOM_VAR
# value: "my-value"
# - name: SECRET_VAR
# valueFrom:
# secretKeyRef:
# name: my-secret
# key: secret-key
extraEnv: []
# Master encryption key (required)
# Generate with: openssl rand -base64 32
# For production, use an existing Kubernetes Secret
masterKey:
existingSecret: "" # Name of existing Kubernetes secret
secretKey: "master-key" # Key in the secret where master key is stored
value: "" # Only used if existingSecret is not set (must be 32-byte base64)
# Optional configuration
# Log level: trace, debug, info, warn, error, fatal
# Defaults to info in production, debug in development
logLevel: ""
# Additional trusted origins (comma-separated)
# Useful when behind a proxy or using multiple domains
# Example: "http://192.168.1.100:3000,https://norish.example.com"
trustedOrigins: ""
# Enable/disable password authentication
# Defaults to false if OIDC or OAuth is configured, true otherwise
passwordAuthEnabled: ""
# Authentication provider configuration
# Configure ONE provider for initial admin account creation
# After first login, manage additional providers via Settings → Admin
auth:
# OIDC/OAuth2 provider
oidc:
enabled: false
name: "MyAuth"
issuer: ""
clientId: ""
clientSecret: ""
# Optional: OIDC well-known configuration URL
# By default derived from issuer by appending /.well-known/openid-configuration
wellKnown: ""
# Use existing secret for OIDC credentials
existingSecret: ""
clientIdKey: "oidc-client-id"
clientSecretKey: "oidc-client-secret"
# GitHub OAuth
github:
enabled: false
clientId: ""
clientSecret: ""
# Use existing secret for GitHub credentials
existingSecret: ""
clientIdKey: "github-client-id"
clientSecretKey: "github-client-secret"
# Google OAuth
google:
enabled: false
clientId: ""
clientSecret: ""
# Use existing secret for Google credentials
existingSecret: ""
clientIdKey: "google-client-id"
clientSecretKey: "google-client-secret"
## External PostgreSQL database configuration (REQUIRED)
## Norish requires a central PostgreSQL database
## You must have a PostgreSQL server available before deploying this chart
database:
# Database connection details
host: "" # Required: PostgreSQL server hostname
port: 5432
name: norish
username: postgres
password: ""
# Use existing secret for database credentials (recommended for production)
existingSecret: "" # Name of existing Kubernetes secret
usernameKey: "username" # Key in the secret for database username
passwordKey: "password" # Key in the secret for database password
databaseKey: "database" # Key in the secret for database name (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)
## Required for improved recipe parsing and scraping
chrome:
enabled: true
image:
repository: zenika/alpine-chrome
tag: "latest"
pullPolicy: IfNotPresent
# Chrome port for remote debugging
port: 9222
# Chrome security context - requires specific capabilities
securityContext:
runAsNonRoot: false
runAsUser: 0
capabilities:
add:
- SYS_ADMIN
# Chrome resource limits
resources: {}
# limits:
# cpu: 500m
# memory: 512Mi
# requests:
# cpu: 100m
# memory: 256Mi

View File

@ -0,0 +1,18 @@
apiVersion: v2
name: paperless-ngx
description: Paperless-ngx helm chart for Kubernetes
type: application
version: 0.0.5
appVersion: "2.20.3"
maintainers:
- name: Richard Tomik
email: richard.tomik@proton.me
keywords:
- productivity
- document-management
- paperless
- paperless-ngx
- ocr
home: https://github.com/rtomik/helm-charts
sources:
- https://github.com/paperless-ngx/paperless-ngx

View File

@ -0,0 +1,92 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "paperless-ngx.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "paperless-ngx.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "paperless-ngx.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "paperless-ngx.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8000 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8000:$CONTAINER_PORT
{{- end }}
2. Application is accessible at port {{ .Values.service.port }}
3. Paperless-ngx is configured with:
- Database: PostgreSQL (external)
- Redis: External service
- OCR Language: {{ .Values.config.ocr.language }}
- Time Zone: {{ .Values.config.timeZone }}
4. External Dependencies Required:
- PostgreSQL server: {{ include "paperless-ngx.postgresql.host" . }}:{{ include "paperless-ngx.postgresql.port" . }}
- Redis server: {{ include "paperless-ngx.redis.host" . }}:{{ include "paperless-ngx.redis.port" . }}
{{- if or .Values.persistence.data.enabled .Values.persistence.media.enabled .Values.persistence.consume.enabled .Values.persistence.export.enabled }}
5. Persistent Storage:
{{- if .Values.persistence.data.enabled }}
- Data directory: {{ include "paperless-ngx.fullname" . }}-data ({{ .Values.persistence.data.size }})
{{- end }}
{{- if .Values.persistence.media.enabled }}
- Media directory: {{ include "paperless-ngx.fullname" . }}-media ({{ .Values.persistence.media.size }})
{{- end }}
{{- if .Values.persistence.consume.enabled }}
- Consume directory: {{ include "paperless-ngx.fullname" . }}-consume ({{ .Values.persistence.consume.size }})
{{- end }}
{{- if .Values.persistence.export.enabled }}
- Export directory: {{ include "paperless-ngx.fullname" . }}-export ({{ .Values.persistence.export.size }})
{{- end }}
{{- else }}
5. WARNING: No persistent storage enabled. Data will be lost when pods are restarted.
Enable persistence in values.yaml for production use.
{{- end }}
{{- if .Values.config.admin.user }}
6. Admin User: {{ .Values.config.admin.user }}
The admin user will be created automatically on first startup.
{{- else }}
6. No admin user configured. You'll need to create a superuser manually:
kubectl exec -it deployment/{{ include "paperless-ngx.fullname" . }} -- python manage.py createsuperuser
{{- end }}
{{- if or .Values.config.secretKey.existingSecret .Values.postgresql.external.existingSecret .Values.config.admin.existingSecret }}
7. Using external secrets for sensitive information:
{{- if .Values.config.secretKey.existingSecret }}
- Secret key from: {{ .Values.config.secretKey.existingSecret }}
{{- end }}
{{- if .Values.postgresql.external.existingSecret }}
- PostgreSQL password from: {{ .Values.postgresql.external.existingSecret }}
{{- end }}
{{- if .Values.config.admin.existingSecret }}
- Admin credentials from: {{ .Values.config.admin.existingSecret }}
{{- end }}
{{- else }}
7. SECURITY NOTE: For production use, it's recommended to store sensitive data in Kubernetes Secrets.
- Set config.secretKey.existingSecret to use an external secret for the secret key
- Set postgresql.external.existingSecret to use an external secret for database credentials
- Set config.admin.existingSecret to use an external secret for admin credentials
{{- end }}
{{- if .Values.config.consumer.barcodes.enabled }}
8. Barcode processing is enabled with scanner: {{ .Values.config.consumer.barcodeScanner }}
{{- end }}
{{- if .Values.config.tika.enabled }}
9. Tika integration is enabled for Office document processing
- Tika endpoint: {{ .Values.config.tika.endpoint }}
- Gotenberg endpoint: {{ .Values.config.tika.gotenbergEndpoint }}
{{- end }}
For more information about using this Helm chart and Paperless-ngx configuration,
please refer to the README.md file and the official Paperless-ngx documentation.

View File

@ -0,0 +1,309 @@
# Paperless-ngx Helm Chart
A Helm chart for deploying [Paperless-ngx](https://github.com/paperless-ngx/paperless-ngx), a document management system with OCR, on Kubernetes.
## Introduction
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.
Source code: https://github.com/rtomik/helm-charts/tree/main/charts/paperless-ngx
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- **External PostgreSQL database** (PostgreSQL 11+ required)
- **External Redis server**
- PV provisioner support
## Installing the Chart
```bash
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install paperless-ngx rtomik/paperless-ngx
```
## Uninstalling the Chart
```bash
helm uninstall paperless-ngx
```
**Note**: PVCs are not deleted automatically. To remove them:
```bash
kubectl delete pvc -l app.kubernetes.io/instance=paperless-ngx
```
## 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
config:
url: "https://paperless.example.com"
allowedHosts: "paperless.example.com"
secretKey:
existingSecret: "paperless-secrets"
secretKey: "django-secret-key"
admin:
user: "admin"
existingSecret: "paperless-admin-secrets"
postgresql:
external:
enabled: true
host: "postgres-cluster-pooler.dbs.svc.cluster.local"
port: 5432
database: "paperless"
username: "paperless"
existingSecret: "paperless-db-credentials"
passwordKey: "password"
redis:
external:
host: "redis.cache.svc.cluster.local"
port: 6379
database: 0
existingSecret: "paperless-redis-credentials"
passwordKey: "password"
prefix: "paperless-prod"
ingress:
enabled: true
className: "nginx"
hosts:
- host: paperless.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: paperless-tls
hosts:
- paperless.example.com
```
### Redis with Username and Password (ACL)
```yaml
redis:
external:
host: "redis.example.com"
username: "paperless-user"
password: "myredispassword"
```
### Sharing Redis Among Multiple Instances
Use the `prefix` parameter to avoid key collisions:
```yaml
# Instance 1
redis:
external:
host: "shared-redis.example.com"
password: "sharedpassword"
prefix: "paperless-prod"
# Instance 2
redis:
external:
host: "shared-redis.example.com"
password: "sharedpassword"
prefix: "paperless-staging"
```
### Using Existing PVCs
```yaml
persistence:
data:
enabled: true
existingClaim: "my-existing-data-pvc"
media:
enabled: true
existingClaim: "my-existing-media-pvc"
export:
enabled: true
consume:
enabled: true
```
When `existingClaim` is set, the chart skips PVC creation and `storageClass`/`size` are ignored for that volume.
## Parameters
### Global Parameters
| Name | Description | Default |
|------|-------------|---------|
| `nameOverride` | Override the release name | `""` |
| `fullnameOverride` | Fully override the release name | `""` |
### 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
- [Paperless-ngx GitHub](https://github.com/paperless-ngx/paperless-ngx)
- [Paperless-ngx Documentation](https://docs.paperless-ngx.com/)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/paperless-ngx)

View File

@ -0,0 +1,130 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "paperless-ngx.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "paperless-ngx.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "paperless-ngx.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "paperless-ngx.labels" -}}
helm.sh/chart: {{ include "paperless-ngx.chart" . }}
{{ include "paperless-ngx.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "paperless-ngx.selectorLabels" -}}
app.kubernetes.io/name: {{ include "paperless-ngx.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
PostgreSQL host
*/}}
{{- define "paperless-ngx.postgresql.host" -}}
{{- if .Values.postgresql.external.enabled }}
{{- .Values.postgresql.external.host }}
{{- else }}
{{- printf "%s-postgresql" (include "paperless-ngx.fullname" .) }}
{{- end }}
{{- end }}
{{/*
PostgreSQL port
*/}}
{{- define "paperless-ngx.postgresql.port" -}}
{{- if .Values.postgresql.external.enabled }}
{{- .Values.postgresql.external.port | toString }}
{{- else }}
{{- "5432" }}
{{- end }}
{{- end }}
{{/*
Redis host
*/}}
{{- define "paperless-ngx.redis.host" -}}
{{- if .Values.redis.external.enabled }}
{{- .Values.redis.external.host }}
{{- else }}
{{- printf "%s-redis" (include "paperless-ngx.fullname" .) }}
{{- end }}
{{- end }}
{{/*
Redis port
*/}}
{{- define "paperless-ngx.redis.port" -}}
{{- if .Values.redis.external.enabled }}
{{- .Values.redis.external.port | toString }}
{{- else }}
{{- "6379" }}
{{- end }}
{{- end }}
{{/*
Redis URL (for non-authenticated Redis)
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
*/}}
{{- define "paperless-ngx.redis.url.withPassword" -}}
{{- $host := include "paperless-ngx.redis.host" . }}
{{- $port := include "paperless-ngx.redis.port" . }}
{{- $database := .Values.redis.external.database | toString }}
{{- $username := .Values.redis.external.username | default "" }}
{{- $password := .Values.redis.external.password | default "" }}
{{- if $username }}
{{- printf "redis://%s:%s@%s:%s/%s" $username $password $host $port $database }}
{{- else }}
{{- printf "redis://:%s@%s:%s/%s" $password $host $port $database }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "paperless-ngx.fullname" . }}-configmap
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
data:
# Additional configuration files can be added here if needed
# Most Paperless-ngx configuration is handled via environment variables
README.txt: |
This ConfigMap can be used to store additional configuration files
for Paperless-ngx if needed. The main configuration is handled via
environment variables in the deployment.

View File

@ -0,0 +1,381 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "paperless-ngx.fullname" . }}
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "paperless-ngx.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
{{- include "paperless-ngx.selectorLabels" . | nindent 8 }}
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
successThreshold: {{ .Values.probes.liveness.successThreshold }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
successThreshold: {{ .Values.probes.readiness.successThreshold }}
{{- end }}
env:
# Required services
{{- if include "paperless-ngx.redis.hasAuth" . }}
# When Redis has authentication, read the full URL from secret
- name: PAPERLESS_REDIS
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 }}
- name: PAPERLESS_REDIS_PREFIX
value: {{ .Values.redis.external.prefix | quote }}
{{- end }}
- name: PAPERLESS_DBHOST
value: {{ include "paperless-ngx.postgresql.host" . | quote }}
- name: PAPERLESS_DBPORT
value: {{ include "paperless-ngx.postgresql.port" . | quote }}
- name: PAPERLESS_DBNAME
value: {{ .Values.postgresql.external.database | quote }}
- name: PAPERLESS_DBUSER
value: {{ .Values.postgresql.external.username | quote }}
# Database password from secret
- name: PAPERLESS_DBPASS
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret | default (printf "%s-secrets" (include "paperless-ngx.fullname" .)) }}
key: {{ .Values.postgresql.external.passwordKey | default "postgresql-password" }}
# Security
- name: PAPERLESS_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.config.secretKey.existingSecret | default (printf "%s-secrets" (include "paperless-ngx.fullname" .)) }}
key: {{ .Values.config.secretKey.secretKey | default "secret-key" }}
# Basic configuration
{{- if .Values.config.url }}
- name: PAPERLESS_URL
value: {{ .Values.config.url | quote }}
{{- end }}
- name: PAPERLESS_ALLOWED_HOSTS
value: {{ .Values.config.allowedHosts | quote }}
{{- if .Values.config.csrfTrustedOrigins }}
- name: PAPERLESS_CSRF_TRUSTED_ORIGINS
value: {{ .Values.config.csrfTrustedOrigins | quote }}
{{- end }}
- name: PAPERLESS_CORS_ALLOWED_HOSTS
value: {{ .Values.config.corsAllowedHosts | quote }}
{{- if .Values.config.forceScriptName }}
- name: PAPERLESS_FORCE_SCRIPT_NAME
value: {{ .Values.config.forceScriptName | quote }}
{{- end }}
# Paths
- name: PAPERLESS_DATA_DIR
value: "/usr/src/paperless/data"
- name: PAPERLESS_MEDIA_ROOT
value: "/usr/src/paperless/media"
- name: PAPERLESS_CONSUMPTION_DIR
value: "/usr/src/paperless/consume"
# Docker/User settings (s6-overlay compatible)
- name: USERMAP_UID
value: "1000"
- name: USERMAP_GID
value: "1000"
# OCR settings
- name: PAPERLESS_OCR_LANGUAGE
value: {{ .Values.config.ocr.language | quote }}
- name: PAPERLESS_OCR_MODE
value: {{ .Values.config.ocr.mode | quote }}
- name: PAPERLESS_OCR_SKIP_ARCHIVE_FILE
value: {{ .Values.config.ocr.skipArchiveFile | quote }}
- name: PAPERLESS_OCR_CLEAN
value: {{ .Values.config.ocr.clean | quote }}
- name: PAPERLESS_OCR_DESKEW
value: {{ .Values.config.ocr.deskew | quote }}
- name: PAPERLESS_OCR_ROTATE_PAGES
value: {{ .Values.config.ocr.rotatePages | quote }}
- name: PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD
value: {{ .Values.config.ocr.rotatePagesThreshold | quote }}
- name: PAPERLESS_OCR_OUTPUT_TYPE
value: {{ .Values.config.ocr.outputType | quote }}
{{- if ne (.Values.config.ocr.pages | int) 0 }}
- name: PAPERLESS_OCR_PAGES
value: {{ .Values.config.ocr.pages | quote }}
{{- end }}
{{- if ne (.Values.config.ocr.imageDpi | int) 0 }}
- name: PAPERLESS_OCR_IMAGE_DPI
value: {{ .Values.config.ocr.imageDpi | quote }}
{{- end }}
{{- if ne (.Values.config.ocr.maxImagePixels | int) 0 }}
- name: PAPERLESS_OCR_MAX_IMAGE_PIXELS
value: {{ .Values.config.ocr.maxImagePixels | quote }}
{{- end }}
{{- if ne .Values.config.ocr.userArgs "{}" }}
- name: PAPERLESS_OCR_USER_ARGS
value: {{ .Values.config.ocr.userArgs | quote }}
{{- end }}
# Time and locale
- name: PAPERLESS_TIME_ZONE
value: {{ .Values.config.timeZone | quote }}
# Consumer settings
- name: PAPERLESS_CONSUMER_RECURSIVE
value: {{ .Values.config.consumer.recursive | quote }}
- name: PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS
value: {{ .Values.config.consumer.subdirsAsTags | quote }}
- name: PAPERLESS_CONSUMER_DELETE_DUPLICATES
value: {{ .Values.config.consumer.deleteDocumentDuplicates | quote }}
- name: PAPERLESS_CONSUMER_IGNORE_PATTERNS
value: {{ .Values.config.consumer.ignorePatterns | quote }}
- name: PAPERLESS_CONSUMER_BARCODE_SCANNER
value: {{ .Values.config.consumer.barcodeScanner | quote }}
# Barcode settings
{{- if .Values.config.consumer.barcodes.enabled }}
- name: PAPERLESS_CONSUMER_ENABLE_BARCODES
value: "true"
- name: PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT
value: {{ .Values.config.consumer.barcodes.tiffSupport | quote }}
- name: PAPERLESS_CONSUMER_BARCODE_STRING
value: {{ .Values.config.consumer.barcodes.string | quote }}
- name: PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
value: {{ .Values.config.consumer.barcodes.retainSplitPages | quote }}
{{- if ne (.Values.config.consumer.barcodes.upscale | float64) 0.0 }}
- name: PAPERLESS_CONSUMER_BARCODE_UPSCALE
value: {{ .Values.config.consumer.barcodes.upscale | quote }}
{{- end }}
- name: PAPERLESS_CONSUMER_BARCODE_DPI
value: {{ .Values.config.consumer.barcodes.dpi | quote }}
{{- if ne (.Values.config.consumer.barcodes.maxPages | int) 0 }}
- name: PAPERLESS_CONSUMER_BARCODE_MAX_PAGES
value: {{ .Values.config.consumer.barcodes.maxPages | quote }}
{{- end }}
{{- end }}
# ASN barcode settings
{{- if .Values.config.consumer.barcodes.asnEnabled }}
- name: PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE
value: "true"
- name: PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX
value: {{ .Values.config.consumer.barcodes.asnPrefix | quote }}
{{- end }}
# Tag barcode settings
{{- if .Values.config.consumer.barcodes.tagEnabled }}
- name: PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE
value: "true"
- name: PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING
value: {{ .Values.config.consumer.barcodes.tagMapping | quote }}
{{- end }}
# Tika settings
{{- if .Values.config.tika.enabled }}
- name: PAPERLESS_TIKA_ENABLED
value: "true"
- name: PAPERLESS_TIKA_ENDPOINT
value: {{ .Values.config.tika.endpoint | quote }}
- name: PAPERLESS_TIKA_GOTENBERG_ENDPOINT
value: {{ .Values.config.tika.gotenbergEndpoint | quote }}
{{- end }}
# Admin user
{{- if .Values.config.admin.user }}
- name: PAPERLESS_ADMIN_USER
value: {{ .Values.config.admin.user | quote }}
- name: PAPERLESS_ADMIN_MAIL
value: {{ .Values.config.admin.email | quote }}
- name: PAPERLESS_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.config.admin.existingSecret | default (printf "%s-secrets" (include "paperless-ngx.fullname" .)) }}
key: {{ .Values.config.admin.passwordKey | default "admin-password" }}
{{- end }}
# Email settings
{{- if .Values.config.email.host }}
- name: PAPERLESS_EMAIL_HOST
value: {{ .Values.config.email.host | quote }}
- name: PAPERLESS_EMAIL_PORT
value: {{ .Values.config.email.port | quote }}
{{- if .Values.config.email.user }}
- name: PAPERLESS_EMAIL_HOST_USER
value: {{ .Values.config.email.user | quote }}
- name: PAPERLESS_EMAIL_HOST_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.config.email.existingSecret | default (printf "%s-secrets" (include "paperless-ngx.fullname" .)) }}
key: {{ .Values.config.email.passwordKey | default "email-password" }}
{{- end }}
{{- if .Values.config.email.from }}
- name: PAPERLESS_EMAIL_FROM
value: {{ .Values.config.email.from | quote }}
{{- end }}
- name: PAPERLESS_EMAIL_USE_TLS
value: {{ .Values.config.email.useTls | quote }}
- name: PAPERLESS_EMAIL_USE_SSL
value: {{ .Values.config.email.useSsl | quote }}
{{- end }}
# Task processing
- name: PAPERLESS_TASK_WORKERS
value: {{ .Values.config.taskWorkers | quote }}
- name: PAPERLESS_THREADS_PER_WORKER
value: {{ .Values.config.threadsPerWorker | quote }}
- name: PAPERLESS_WORKER_TIMEOUT
value: {{ .Values.config.workerTimeout | quote }}
# Advanced settings
- name: PAPERLESS_ENABLE_NLTK
value: {{ .Values.config.enableNltk | quote }}
{{- if .Values.config.filenameFormat }}
- name: PAPERLESS_FILENAME_FORMAT
value: {{ .Values.config.filenameFormat | quote }}
- name: PAPERLESS_FILENAME_FORMAT_REMOVE_NONE
value: {{ .Values.config.filenameFormatRemoveNone | quote }}
{{- end }}
{{- if ne (.Values.config.convertMemoryLimit | int) 0 }}
- name: PAPERLESS_CONVERT_MEMORY_LIMIT
value: {{ .Values.config.convertMemoryLimit | quote }}
{{- end }}
{{- if .Values.config.convertTmpDir }}
- name: PAPERLESS_CONVERT_TMPDIR
value: {{ .Values.config.convertTmpDir | quote }}
{{- end }}
{{- if ne (.Values.config.maxImagePixels | int) 0 }}
- name: PAPERLESS_MAX_IMAGE_PIXELS
value: {{ .Values.config.maxImagePixels | quote }}
{{- end }}
# Custom environment variables
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.extraEnvFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /usr/src/paperless/data
- name: media
mountPath: /usr/src/paperless/media
- name: export
mountPath: /usr/src/paperless/export
- name: consume
mountPath: /usr/src/paperless/consume
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
{{- if .Values.persistence.data.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ if .Values.persistence.data.existingClaim }}{{ .Values.persistence.data.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-data{{ end }}
{{- else }}
- name: data
emptyDir: {}
{{- end }}
{{- if .Values.persistence.media.enabled }}
- name: media
persistentVolumeClaim:
claimName: {{ if .Values.persistence.media.existingClaim }}{{ .Values.persistence.media.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-media{{ end }}
{{- else }}
- name: media
emptyDir: {}
{{- end }}
{{- if .Values.persistence.export.enabled }}
- name: export
persistentVolumeClaim:
claimName: {{ if .Values.persistence.export.existingClaim }}{{ .Values.persistence.export.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-export{{ end }}
{{- else }}
- name: export
emptyDir: {}
{{- end }}
{{- if .Values.persistence.consume.enabled }}
- name: consume
persistentVolumeClaim:
claimName: {{ if .Values.persistence.consume.existingClaim }}{{ .Values.persistence.consume.existingClaim }}{{ else }}{{ include "paperless-ngx.fullname" . }}-consume{{ end }}
{{- else }}
- name: consume
emptyDir: {}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "paperless-ngx.fullname" . }}
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "paperless-ngx.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,90 @@
{{- if and .Values.persistence.data.enabled (not .Values.persistence.data.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "paperless-ngx.fullname" . }}-data
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
{{- with .Values.persistence.data.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.data.accessMode | quote }}
{{- if .Values.persistence.data.storageClass }}
storageClassName: {{ .Values.persistence.data.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.data.size | quote }}
---
{{- end }}
{{- if and .Values.persistence.media.enabled (not .Values.persistence.media.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "paperless-ngx.fullname" . }}-media
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
{{- with .Values.persistence.media.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.media.accessMode | quote }}
{{- if .Values.persistence.media.storageClass }}
storageClassName: {{ .Values.persistence.media.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.media.size | quote }}
---
{{- end }}
{{- if and .Values.persistence.export.enabled (not .Values.persistence.export.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "paperless-ngx.fullname" . }}-export
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
{{- with .Values.persistence.export.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.export.accessMode | quote }}
{{- if .Values.persistence.export.storageClass }}
storageClassName: {{ .Values.persistence.export.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.export.size | quote }}
---
{{- end }}
{{- if and .Values.persistence.consume.enabled (not .Values.persistence.consume.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "paperless-ngx.fullname" . }}-consume
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
{{- with .Values.persistence.consume.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.consume.accessMode | quote }}
{{- if .Values.persistence.consume.storageClass }}
storageClassName: {{ .Values.persistence.consume.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.consume.size | quote }}
{{- end }}

View File

@ -0,0 +1,45 @@
{{- $needsSecret := false -}}
{{- if not .Values.config.secretKey.existingSecret -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if not .Values.postgresql.external.existingSecret -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if and .Values.redis.external.password (not .Values.redis.external.existingSecret) -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if and .Values.config.admin.user (not .Values.config.admin.existingSecret) -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if and .Values.config.email.host .Values.config.email.user (not .Values.config.email.existingSecret) -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if $needsSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "paperless-ngx.fullname" . }}-secrets
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
type: Opaque
data:
{{- if not .Values.config.secretKey.existingSecret }}
{{ .Values.config.secretKey.secretKey | default "secret-key" }}: {{ .Values.config.secretKey.value | default "change-me-paperless-secret-key-at-least-32-characters-long" | b64enc }}
{{- end }}
{{- if not .Values.postgresql.external.existingSecret }}
{{ .Values.postgresql.external.passwordKey | default "postgresql-password" }}: {{ .Values.postgresql.external.password | default "paperless" | b64enc }}
{{- end }}
{{- 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.urlKey | default "redis-url" }}: {{ include "paperless-ngx.redis.url.withPassword" . | b64enc }}
{{- end }}
{{- 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.passwordKey | default "admin-password" }}: {{ .Values.config.admin.password | default "changeme" | b64enc }}
{{- end }}
{{- if and .Values.config.email.host .Values.config.email.user (not .Values.config.email.existingSecret) }}
{{ .Values.config.email.userKey | default "email-user" }}: {{ .Values.config.email.user | b64enc }}
{{ .Values.config.email.passwordKey | default "email-password" }}: {{ .Values.config.email.password | default "" | b64enc }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "paperless-ngx.fullname" . }}
labels:
{{- include "paperless-ngx.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "paperless-ngx.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,302 @@
## Global settings
nameOverride: ""
fullnameOverride: ""
## Image settings
image:
repository: ghcr.io/paperless-ngx/paperless-ngx
tag: "2.20.3"
pullPolicy: IfNotPresent
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
# Pod security settings
# Note: Paperless-ngx uses s6-overlay which requires root access during initialization
# The container will drop privileges after setup
podSecurityContext:
runAsNonRoot: false
runAsUser: 0
fsGroup: 1000
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
## Pod scheduling
nodeSelector: {}
tolerations: []
affinity: {}
## Service settings
service:
type: ClusterIP
port: 8000
## Ingress settings
ingress:
enabled: false
className: ""
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: paperless.domain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- paperless.domain.com
# Optional: specify the name of an existing TLS secret
# secretName: "existing-tls-secret"
## Persistence settings
persistence:
# Paperless data directory (search index, classification model, etc.)
data:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
annotations: {}
# Paperless media directory (documents and thumbnails)
media:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 10Gi
annotations: {}
# Export directory (for exporting documents)
export:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
annotations: {}
# Consume directory (for importing documents)
consume:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
annotations: {}
# Extra volume mounts
extraVolumeMounts: []
# Extra volumes
extraVolumes: []
## Resource limits and requests
# resources:
# limits:
# cpu: 1000m
# memory: 1Gi
# requests:
# cpu: 200m
# memory: 512Mi
## Application health checks
probes:
liveness:
enabled: true
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
path: /
readiness:
enabled: true
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
path: /
## Autoscaling configuration
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
## External Dependencies Configuration
## These should point to external PostgreSQL and Redis services
# External PostgreSQL database configuration
postgresql:
# External PostgreSQL connection details
external:
enabled: true
host: "postgresql.default.svc.cluster.local"
port: 5432
database: "paperless"
username: "paperless"
# Use existingSecret for credentials
existingSecret: ""
passwordKey: "postgresql-password"
# Or set password directly (not recommended for production)
password: ""
# External Redis configuration
redis:
external:
enabled: true
host: "redis.default.svc.cluster.local"
port: 6379
database: 0
# Authentication (leave empty if Redis has no auth)
username: "" # Optional: Redis username (Redis 6.0+)
# 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: ""
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)
# When using plain password, the full Redis URL will be auto-generated in the secret
password: ""
# Optional: Prefix for Redis keys and channels
# Useful for sharing one Redis server among multiple Paperless instances
prefix: ""
## Paperless-ngx Configuration
config:
# Basic server configuration
url: "" # Set to your external URL, e.g., https://paperless.domain.com
allowedHosts: "*" # Comma-separated list of allowed hosts
csrfTrustedOrigins: "" # Comma-separated list of trusted origins
corsAllowedHosts: "http://localhost:8000"
forceScriptName: "" # For hosting under subpath, e.g., /paperless
# Security settings
secretKey:
# Use existingSecret for production
existingSecret: ""
secretKey: "secret-key"
# Or set directly (not recommended for production)
value: ""
# OCR Configuration
ocr:
language: "eng" # OCR language (3-letter code)
mode: "skip" # skip, redo, or force
skipArchiveFile: "never" # never, with_text, always
clean: "clean" # clean, clean-final, none
deskew: true
rotatePages: true
rotatePagesThreshold: 12
outputType: "pdfa"
pages: 0 # 0 = all pages
imageDpi: 0 # 0 = auto
maxImagePixels: 0 # 0 = use Pillow default
userArgs: "{}" # JSON string of additional OCRmyPDF arguments
# Time and locale settings
timeZone: "UTC"
# Consumer settings
consumer:
recursive: false
subdirsAsTags: false
deleteDocumentDuplicates: false
ignorePatterns: '[".DS_Store", ".DS_STORE", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini", "@eaDir/*", "Thumbs.db"]'
barcodeScanner: "PYZBAR"
# Barcode processing
barcodes:
enabled: false
tiffSupport: false
string: "PATCHT"
retainSplitPages: false
upscale: 0.0
dpi: 300
maxPages: 0
# ASN barcode settings
asnEnabled: false
asnPrefix: "ASN"
# Tag barcode settings
tagEnabled: false
tagMapping: '{"TAG:(.*)": "\\g<1>"}'
# Optional Tika settings (for Office documents)
tika:
enabled: false
endpoint: "http://tika:9998"
gotenbergEndpoint: "http://gotenberg:3000"
# Admin user creation (optional)
admin:
user: "" # Set to create admin user on startup
password: "" # Required if admin.user is set
email: "root@localhost"
# Use existingSecret for credentials
existingSecret: ""
userKey: "admin-user"
passwordKey: "admin-password"
# Email configuration (optional)
email:
host: ""
port: 25
user: ""
password: ""
from: ""
useTls: false
useSsl: false
# Use existingSecret for credentials
existingSecret: ""
userKey: "email-user"
passwordKey: "email-password"
# Logging
logging:
dir: "" # Uses PAPERLESS_DATA_DIR/log/ if empty
# Task processing
taskWorkers: 1
threadsPerWorker: 1
workerTimeout: 1800
# Advanced settings
filenameFormat: ""
filenameFormatRemoveNone: false
enableNltk: true
convertMemoryLimit: 0
convertTmpDir: ""
maxImagePixels: 0
# Environment variables
env: []
# Example additional env vars:
# - name: PAPERLESS_ENABLE_HTTP_REMOTE_USER
# value: "false"
# Extra environment variables from secrets
extraEnvFrom: []
# - secretRef:
# name: paperless-extra-secrets
# Extra environment variables (for advanced use cases)
extraEnv: []

View File

@ -0,0 +1,18 @@
apiVersion: v2
name: qbittorrent-vpn
description: qBittorrent with Gluetun VPN sidecar for Kubernetes
type: application
version: 0.0.2
appVersion: 5.1.0
maintainers:
- name: Richard Tomik
email: richard.tomik@proton.me
keywords:
- qbittorrent
- vpn
- gluetun
- torrent
home: https://github.com/rtomik/helm-charts
sources:
- https://github.com/linuxserver/docker-qbittorrent
- https://github.com/qdm12/gluetun

View File

@ -0,0 +1,38 @@
Thank you for installing {{ .Chart.Name }}.
Your qBittorrent with VPN has been deployed successfully!
1. Get the application URL:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "qbittorrent-vpn.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "qbittorrent-vpn.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "qbittorrent-vpn.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "qbittorrent-vpn.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
Visit http://127.0.0.1:{{ .Values.service.port }} to access qBittorrent
{{- end }}
2. VPN Status:
To check the VPN connection status:
kubectl exec -it -n {{ .Release.Namespace }} deployment/{{ include "qbittorrent-vpn.fullname" . }} -c gluetun -- curl -s http://localhost:8000/v1/vpn/status
3. Public IP:
To check your current public IP through the VPN:
kubectl exec -it -n {{ .Release.Namespace }} deployment/{{ include "qbittorrent-vpn.fullname" . }} -c gluetun -- curl -s http://localhost:8000/v1/publicip/ip
4. Verify qBittorrent:
Make sure qBittorrent is functioning by accessing the Web UI at the URL in step 1.
For more information about this chart:
https://github.com/rtomik/helm-charts/tree/main/charts/qbittorrent-vpn

View File

@ -0,0 +1,303 @@
# qBittorrent VPN Helm Chart
A Helm chart for deploying [qBittorrent](https://www.qbittorrent.org/) with a [Gluetun](https://github.com/qdm12/gluetun) VPN sidecar on Kubernetes.
## Introduction
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: https://github.com/rtomik/helm-charts/tree/main/charts/qbittorrent-vpn
**Note**: Currently only tested with NordVPN and OpenVPN configuration.
## Prerequisites
- Kubernetes 1.19+
- Helm 3.2.0+
- PV provisioner support in the cluster
- A valid VPN subscription
## Installing the Chart
```bash
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install qbittorrent-vpn rtomik/qbittorrent-vpn
```
## Uninstalling the Chart
```bash
helm uninstall qbittorrent-vpn
```
**Note**: PVCs are not deleted automatically. To remove them:
```bash
kubectl delete pvc -l app.kubernetes.io/instance=qbittorrent-vpn
```
## Configuration Examples
### NordVPN with Existing Secret
First, create a secret with your VPN credentials:
```bash
kubectl create secret generic vpn-credentials \
--from-literal=username='your-vpn-username' \
--from-literal=password='your-vpn-password'
```
```yaml
gluetun:
vpn:
provider: "nordvpn"
type: "openvpn"
serverCountries: "United States"
openvpn:
NORDVPN_CATEGORY: "P2P"
credentials:
create: false
existingSecret: "vpn-credentials"
usernameKey: "username"
passwordKey: "password"
ingress:
enabled: true
hosts:
- host: qbittorrent.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- qbittorrent.example.com
```
### ProtonVPN
```yaml
gluetun:
vpn:
provider: "protonvpn"
type: "openvpn"
serverCountries: "Switzerland"
openvpn:
PROTONVPN_TIER: "2"
SERVER_FEATURES: "p2p"
credentials:
create: true
username: "protonvpn-username"
password: "protonvpn-password"
```
### Private Internet Access with Port Forwarding
```yaml
gluetun:
vpn:
provider: "private internet access"
type: "openvpn"
serverCountries: "US"
credentials:
create: true
username: "pia-username"
password: "pia-password"
settings:
VPN_PORT_FORWARDING: "on"
STATUS_FILE: "/tmp/gluetun-status.json"
```
### Proxy Services
```yaml
service:
proxies:
enabled: true
httpPort: 8888
socksPort: 8388
```
### Custom Sidecar (NATMap)
Sidecars can access shared volumes: `config`, `downloads`, and `gluetun-config`.
```yaml
sidecars:
- name: natmap
image: ghcr.io/muink/natmap:latest
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
```
## Parameters
### qBittorrent Parameters
| Name | Description | Default |
|------|-------------|---------|
| `qbittorrent.image.repository` | qBittorrent image repository | `linuxserver/qbittorrent` |
| `qbittorrent.image.tag` | qBittorrent image tag | `5.1.0` |
| `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
### VPN Not Connecting
```bash
kubectl logs deployment/qbittorrent-vpn -c gluetun
kubectl describe secret vpn-credentials
```
Enable debug logging for more detail:
```yaml
gluetun:
extraEnv:
- name: LOG_LEVEL
value: "debug"
```
### Directory Creation Errors
Ensure the init container is enabled and `fsGroup` is set in `podSecurityContext`.
### Firewall / Network Issues
Gluetun requires `privileged: true` and `NET_ADMIN` capability. Verify `/dev/net/tun` is mounted correctly.
## Links
- [qBittorrent](https://www.qbittorrent.org/)
- [Gluetun GitHub](https://github.com/qdm12/gluetun)
- [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)

View File

@ -0,0 +1,45 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "qbittorrent-vpn.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "qbittorrent-vpn.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "qbittorrent-vpn.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "qbittorrent-vpn.labels" -}}
helm.sh/chart: {{ include "qbittorrent-vpn.chart" . }}
{{ include "qbittorrent-vpn.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "qbittorrent-vpn.selectorLabels" -}}
app.kubernetes.io/name: {{ include "qbittorrent-vpn.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@ -0,0 +1,310 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "qbittorrent-vpn.fullname" . }}
labels:
{{- include "qbittorrent-vpn.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "qbittorrent-vpn.selectorLabels" . | nindent 6 }}
strategy:
type: Recreate # Using Recreate instead of RollingUpdate for stateful pods
template:
metadata:
labels:
{{- include "qbittorrent-vpn.selectorLabels" . | nindent 8 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
# Add hostNetwork if specified
{{- if .Values.hostNetwork }}
hostNetwork: {{ .Values.hostNetwork }}
{{- end }}
# Init containers if needed for directory setup
{{- if .Values.initContainers }}
initContainers:
{{- toYaml .Values.initContainers | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
{{- if .Values.gluetun.enabled }}
# Gluetun VPN container
- name: gluetun
image: "{{ .Values.gluetun.image.repository }}:{{ .Values.gluetun.image.tag }}"
imagePullPolicy: {{ .Values.gluetun.image.pullPolicy }}
securityContext:
{{- toYaml .Values.gluetun.securityContext | nindent 12 }}
env:
# VPN Provider selection - Common settings for all VPN types
- name: VPN_SERVICE_PROVIDER
value: {{ .Values.gluetun.vpn.provider | quote }}
- name: VPN_TYPE
value: {{ .Values.gluetun.vpn.type | quote }}
- name: SERVER_COUNTRIES
value: {{ .Values.gluetun.vpn.serverCountries | quote }}
{{- if .Values.gluetun.vpn.serverNames }}
- name: SERVER_HOSTNAMES
value: {{ .Values.gluetun.vpn.serverNames | quote }}
{{- end }}
{{- if .Values.gluetun.vpn.serverCities }}
- name: SERVER_CITIES
value: {{ .Values.gluetun.vpn.serverCities | quote }}
{{- end }}
{{- if .Values.gluetun.vpn.randomize }}
- name: SERVER_HOSTNAMES_RANDOMIZED
value: {{ .Values.gluetun.vpn.randomize | quote }}
{{- end }}
# OpenVPN specific configuration
{{- if eq .Values.gluetun.vpn.type "openvpn" }}
{{- if .Values.gluetun.credentials.create }}
- name: OPENVPN_USER
valueFrom:
secretKeyRef:
name: {{ include "qbittorrent-vpn.fullname" . }}-vpn-credentials
key: {{ .Values.gluetun.credentials.usernameKey }}
- name: OPENVPN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "qbittorrent-vpn.fullname" . }}-vpn-credentials
key: {{ .Values.gluetun.credentials.passwordKey }}
{{- else if .Values.gluetun.credentials.existingSecret }}
- name: OPENVPN_USER
valueFrom:
secretKeyRef:
name: {{ .Values.gluetun.credentials.existingSecret }}
key: {{ .Values.gluetun.credentials.usernameKey }}
- name: OPENVPN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.gluetun.credentials.existingSecret }}
key: {{ .Values.gluetun.credentials.passwordKey }}
{{- end }}
# Additional OpenVPN settings
{{- with .Values.gluetun.vpn.openvpn }}
{{- range $key, $value := . }}
- name: {{ $key | upper }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
# WireGuard specific configuration
{{- if eq .Values.gluetun.vpn.type "wireguard" }}
{{- if and .Values.gluetun.vpn.wireguard.privateKey .Values.gluetun.credentials.create }}
- name: WIREGUARD_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: {{ include "qbittorrent-vpn.fullname" . }}-vpn-credentials
key: wireguard_private_key
{{- else if and .Values.gluetun.vpn.wireguard.privateKeyExistingSecret .Values.gluetun.vpn.wireguard.privateKeyExistingSecretKey }}
- name: WIREGUARD_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.gluetun.vpn.wireguard.privateKeyExistingSecret }}
key: {{ .Values.gluetun.vpn.wireguard.privateKeyExistingSecretKey }}
{{- end }}
# Additional WireGuard settings
{{- with .Values.gluetun.vpn.wireguard }}
{{- if .addresses }}
- name: WIREGUARD_ADDRESSES
value: {{ .addresses | quote }}
{{- end }}
{{- if .endpointIP }}
- name: WIREGUARD_ENDPOINT_IP
value: {{ .endpointIP | quote }}
{{- end }}
{{- if .endpointPort }}
- name: WIREGUARD_ENDPOINT_PORT
value: {{ .endpointPort | quote }}
{{- end }}
{{- if .publicKey }}
- name: WIREGUARD_PUBLIC_KEY
value: {{ .publicKey | quote }}
{{- end }}
{{- end }}
{{- end }}
# Gluetun general settings
{{- with .Values.gluetun.settings }}
{{- range $key, $value := . }}
- name: {{ $key | upper }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
# Extra environment variables
{{- with .Values.gluetun.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: control
containerPort: 8000
protocol: TCP
- name: http-proxy
containerPort: 8888
protocol: TCP
- name: shadowsocks-tcp
containerPort: 8388
protocol: TCP
- name: shadowsocks-udp
containerPort: 8388
protocol: UDP
{{- with .Values.gluetun.extraPorts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
# Mount tun device for VPN
- name: tun
mountPath: /dev/net/tun
{{- if .Values.gluetun.persistence.enabled }}
- name: gluetun-config
mountPath: /gluetun
{{- end }}
{{- with .Values.gluetun.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.gluetun.resources | nindent 12 }}
{{- end }}
# qBittorrent container
- name: qbittorrent
image: "{{ .Values.qbittorrent.image.repository }}:{{ .Values.qbittorrent.image.tag }}"
imagePullPolicy: {{ .Values.qbittorrent.image.pullPolicy }}
{{- if .Values.qbittorrent.securityContext }}
securityContext:
{{- toYaml .Values.qbittorrent.securityContext | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.qbittorrent.service.port }}
protocol: TCP
{{- if .Values.qbittorrent.bittorrentPort }}
- name: bittorrent-tcp
containerPort: {{ .Values.qbittorrent.bittorrentPort }}
protocol: TCP
- name: bittorrent-udp
containerPort: {{ .Values.qbittorrent.bittorrentPort }}
protocol: UDP
{{- end }}
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
successThreshold: {{ .Values.probes.liveness.successThreshold }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
successThreshold: {{ .Values.probes.readiness.successThreshold }}
{{- end }}
env:
{{- range .Values.qbittorrent.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- with .Values.qbittorrent.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
{{- if .Values.qbittorrent.persistence.config.enabled }}
- name: config
mountPath: {{ .Values.qbittorrent.persistence.config.mountPath }}
{{- end }}
{{- if .Values.qbittorrent.persistence.downloads.enabled }}
- name: downloads
mountPath: {{ .Values.qbittorrent.persistence.downloads.mountPath }}
{{- end }}
{{- with .Values.qbittorrent.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.qbittorrent.resources | nindent 12 }}
{{- with .Values.sidecars }}
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
# Create /dev/net/tun as a device
- name: tun
hostPath:
path: /dev/net/tun
type: CharDevice
{{- if .Values.qbittorrent.persistence.config.enabled }}
- name: config
persistentVolumeClaim:
claimName: {{ if .Values.qbittorrent.persistence.config.existingClaim }}{{ .Values.qbittorrent.persistence.config.existingClaim }}{{ else }}{{ include "qbittorrent-vpn.fullname" . }}-config{{ end }}
{{- end }}
{{- if .Values.qbittorrent.persistence.downloads.enabled }}
- name: downloads
persistentVolumeClaim:
claimName: {{ if .Values.qbittorrent.persistence.downloads.existingClaim }}{{ .Values.qbittorrent.persistence.downloads.existingClaim }}{{ else }}{{ include "qbittorrent-vpn.fullname" . }}-downloads{{ end }}
{{- end }}
{{- if and .Values.gluetun.enabled .Values.gluetun.persistence.enabled }}
{{- if .Values.gluetun.persistence.useEmptyDir }}
- name: gluetun-config
emptyDir: {}
{{- else }}
- name: gluetun-config
persistentVolumeClaim:
claimName: {{ if .Values.gluetun.persistence.existingClaim }}{{ .Values.gluetun.persistence.existingClaim }}{{ else }}{{ include "qbittorrent-vpn.fullname" . }}-gluetun{{ end }}
{{- end }}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "qbittorrent-vpn.fullname" . }}
labels:
{{- include "qbittorrent-vpn.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "qbittorrent-vpn.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,55 @@
{{- if and .Values.qbittorrent.persistence.config.enabled (not .Values.qbittorrent.persistence.config.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "qbittorrent-vpn.fullname" . }}-config
labels:
{{- include "qbittorrent-vpn.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.qbittorrent.persistence.config.accessMode | quote }}
{{- if .Values.qbittorrent.persistence.config.storageClass }}
storageClassName: {{ .Values.qbittorrent.persistence.config.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.qbittorrent.persistence.config.size | quote }}
{{- end }}
{{- if and .Values.qbittorrent.persistence.downloads.enabled (not .Values.qbittorrent.persistence.downloads.existingClaim) }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "qbittorrent-vpn.fullname" . }}-downloads
labels:
{{- include "qbittorrent-vpn.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.qbittorrent.persistence.downloads.accessMode | quote }}
{{- if .Values.qbittorrent.persistence.downloads.storageClass }}
storageClassName: {{ .Values.qbittorrent.persistence.downloads.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.qbittorrent.persistence.downloads.size | quote }}
{{- end }}
{{- if and .Values.gluetun.enabled .Values.gluetun.persistence.enabled (not .Values.gluetun.persistence.useEmptyDir) (not .Values.gluetun.persistence.existingClaim) }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "qbittorrent-vpn.fullname" . }}-gluetun
labels:
{{- include "qbittorrent-vpn.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.gluetun.persistence.accessMode | quote }}
{{- if .Values.gluetun.persistence.storageClass }}
storageClassName: {{ .Values.gluetun.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.gluetun.persistence.size | quote }}
{{- end }}

View File

@ -0,0 +1,18 @@
{{- if and .Values.gluetun.enabled .Values.gluetun.credentials.create }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "qbittorrent-vpn.fullname" . }}-vpn-credentials
labels:
{{- include "qbittorrent-vpn.labels" . | nindent 4 }}
type: Opaque
data:
{{- if eq .Values.gluetun.vpn.type "openvpn" }}
{{ .Values.gluetun.credentials.usernameKey }}: {{ .Values.gluetun.credentials.username | b64enc | quote }}
{{ .Values.gluetun.credentials.passwordKey }}: {{ .Values.gluetun.credentials.password | b64enc | quote }}
{{- end }}
{{- if and (eq .Values.gluetun.vpn.type "wireguard") .Values.gluetun.vpn.wireguard.privateKey }}
wireguard_private_key: {{ .Values.gluetun.vpn.wireguard.privateKey | b64enc | quote }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "qbittorrent-vpn.fullname" . }}
labels:
{{- include "qbittorrent-vpn.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "qbittorrent-vpn.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,245 @@
## Global settings
nameOverride: ""
fullnameOverride: ""
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
## Pod security settings
podSecurityContext:
runAsNonRoot: false
runAsUser: 0 # Run all containers as root
fsGroup: 0 # Use root group for volumes
## qBittorrent Image settings
qbittorrent:
image:
repository: linuxserver/qbittorrent
tag: 5.1.0
pullPolicy: IfNotPresent
securityContext: {}
# Open port for BitTorrent traffic
bittorrentPort: 6881
env:
- name: PUID
value: "0" # Run as root
- name: PGID
value: "0" # Root group
- name: TZ
value: "UTC"
- name: WEBUI_PORT
value: "8080"
extraEnv: []
service:
port: 8080
#resources:
# limits:
# cpu: 1000m
# memory: 2Gi
# requests:
# cpu: 200m
# memory: 512Mi
persistence:
config:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 2Gi
mountPath: /config
downloads:
enabled: true
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 2Gi
mountPath: /downloads
# Volume mounts specific to qBittorrent
extraVolumeMounts: []
# Volumes specific to qBittorrent
extraVolumes: []
# Probes for qBittorrent
probes:
liveness:
enabled: true
path: /
initialDelaySeconds: 0 # Startup probe handles delayed start
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
readiness:
enabled: true
path: /
initialDelaySeconds: 0 # Startup probe handles delayed start
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
## Gluetun VPN settings
gluetun:
enabled: true
image:
repository: qmcgaw/gluetun
tag: v3.40.0 # Latest version as of this writing
pullPolicy: IfNotPresent
securityContext:
privileged: true
capabilities:
add:
- NET_ADMIN
# VPN provider configuration
vpn:
# Choose from: nordvpn, protonvpn, expressvpn, surfshark, mullvad, ivpn, private internet access, etc.
provider: "nordvpn"
# Choose from: openvpn or wireguard
type: "openvpn"
# Server selection (comma-separated lists)
serverCountries: "Netherlands" # e.g., "Netherlands,Germany,Sweden"
serverCities: "" # e.g., "Amsterdam,Frankfurt" (optional)
serverNames: "" # e.g., "nl1,nl2" (optional)
randomize: "true" # Randomize server selection
# OpenVPN specific settings (when type is "openvpn")
openvpn:
# Add any OpenVPN specific settings here, they'll be converted to env vars
OPENVPN_PROTOCOL: "udp"
# WireGuard specific settings (when type is "wireguard")
wireguard:
privateKey: "" # Will be stored in Secret if provided
privateKeyExistingSecret: ""
privateKeyExistingSecretKey: ""
addresses: "" # e.g., "10.64.222.21/32"
endpointIP: "" # Optional: specify endpoint IP
endpointPort: "" # Optional: specify endpoint port
publicKey: "" # Optional: server public key
# VPN credentials (choose one method)
credentials:
create: true # set to false if using existing secret
# For OpenVPN (normal credentials)
username: ""
password: ""
# For WireGuard, the privateKey is specified in vpn.wireguard.privateKey
# Alternatively, reference an existing secret
existingSecret: ""
usernameKey: "username"
passwordKey: "password"
# General Gluetun settings as environment variables
settings:
FIREWALL: "on"
FIREWALL_OUTBOUND_SUBNETS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
DNS_ADDRESS: "1.1.1.1"
HEALTH_SERVER_PORT: "8000"
# Important: Add these settings to make networking work correctly with ingress
SERVER_ALLOWLIST: "qbittorrent:8080" # Allow accessing qBittorrent container
FIREWALL_INPUT_PORTS: "8080" # Allow ingress traffic to port 8080
FIREWALL_DEBUG: "on" # Enable firewall debugging (temporarily)
JOURNALD: "off" # Disable journald (not needed for debugging)
# Optional port forwarding
VPN_PORT_FORWARDING: "off"
# Extra environment variables
extraEnv:
- name: LOG_LEVEL
value: "info"
# Extra ports to expose
extraPorts: []
# - name: custom-port
# containerPort: 9999
# protocol: TCP
# Resources for Gluetun
resources:
limits:
cpu: 300m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
# Persistence for Gluetun
persistence:
enabled: true
existingClaim: false
storageClass: ""
accessMode: ReadWriteOnce
size: 100Mi
# Volume mounts specific to Gluetun
extraVolumeMounts: []
# Volumes specific to Gluetun
extraVolumes: []
## Service settings
service:
type: ClusterIP
port: 8080
## Ingress settings
ingress:
enabled: false
className: ""
annotations: []
hosts:
- host: qbittorrent.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- qbittorrent.example.com
# Additional specifications
nodeSelector: {}
tolerations: []
affinity: {}
podAnnotations: {}
extraVolumes: []
# Temporary options for development/debugging
hostNetwork: false
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,6 +1,6 @@
apiVersion: v2
name: recipya
description: A Helm chart for Recipya recipe manager application
description: Recipya helm chart for Kubernetes
type: application
version: 0.0.2
appVersion: "v1.2.2"

View File

@ -1,137 +1,43 @@
# Recipya Helm Chart
A Helm chart for deploying [Recipya](https://github.com/reaper47/recipya) on Kubernetes.
[Source Code](https://github.com/rtomik/helm-charts/tree/main/charts%2Frecipya)
A Helm chart for deploying [Recipya](https://github.com/reaper47/recipya), a recipe management application, on Kubernetes.
## 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: https://github.com/rtomik/helm-charts/tree/main/charts/recipya
## Prerequisites
- Kubernetes 1.19+
- 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
To install the chart with the release name `recipya`:
```bash
helm repo add recipya-chart https://rtomik.github.io/helm-charts
helm install recipya recipya-chart/recipya -n recipya
helm repo add rtomik https://rtomik.github.io/helm-charts
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
To uninstall/delete the `recipya` deployment:
```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
config:
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:
enabled: true
className: "traefik"
@ -147,37 +53,19 @@ ingress:
pathType: ImplementationSpecific
tls:
- hosts:
- recipya.example.com
- recipya.example.com
```
This configuration includes:
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:
### With SendGrid Email
```yaml
contentSecurityPolicy: >-
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:;
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';
config:
email:
address: "your-email@example.com"
sendgrid: "SG.your-sendgrid-api-key"
```
## Using Existing Secrets
If you want to use existing secrets for sensitive data:
### With SendGrid and Azure Document Intelligence via Existing Secrets
```yaml
config:
@ -191,6 +79,127 @@ config:
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)

18
charts/tandoor/Chart.yaml Normal file
View File

@ -0,0 +1,18 @@
apiVersion: v2
name: tandoor
description: Tandoor Recipes - A recipe management application for Kubernetes
type: application
version: 0.0.1
appVersion: "2.3.5"
maintainers:
- name: Richard Tomik
email: no@m.com
keywords:
- recipes
- cooking
- meal-planning
- tandoor
- food
home: https://github.com/rtomik/helm-charts
sources:
- https://github.com/TandoorRecipes/recipes

455
charts/tandoor/readme.md Normal file
View File

@ -0,0 +1,455 @@
# Tandoor Recipes Helm Chart
A Helm chart for deploying [Tandoor Recipes](https://github.com/TandoorRecipes/recipes), a recipe management application, on Kubernetes.
## Introduction
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.
Source code: https://github.com/rtomik/helm-charts/tree/main/charts/tandoor
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- **External PostgreSQL database** (required — this chart does not include PostgreSQL)
- PV provisioner support
## Installing the Chart
```bash
helm repo add rtomik https://rtomik.github.io/helm-charts
helm install tandoor rtomik/tandoor
```
## Uninstalling the Chart
```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
postgresql:
host: "postgresql.database.svc.cluster.local"
database: "tandoor"
username: "tandoor"
password: "your-secure-password"
config:
secretKey:
value: "your-secret-key-at-least-50-characters-long-for-security-purposes"
```
### Production with Existing Secrets
```yaml
postgresql:
host: "postgresql.database.svc.cluster.local"
database: "tandoor"
username: "tandoor"
existingSecret: "tandoor-db-secret"
passwordKey: "password"
config:
secretKey:
existingSecret: "tandoor-app-secret"
secretKey: "secret-key"
allowedHosts: "tandoor.example.com"
csrfTrustedOrigins: "https://tandoor.example.com"
timezone: "Europe/Berlin"
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: tandoor.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- tandoor.example.com
secretName: tandoor-tls
persistence:
staticfiles:
enabled: true
storageClass: "longhorn"
size: 2Gi
mediafiles:
enabled: true
storageClass: "longhorn"
size: 10Gi
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
```
### OIDC with Authentik
```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"
```
### S3 Object Storage
```yaml
config:
s3:
enabled: true
bucketName: "tandoor-media"
regionName: "us-east-1"
endpointUrl: "https://minio.example.com"
existingSecret: "tandoor-s3-secret"
```
### Email Configuration
```yaml
config:
email:
host: "smtp.example.com"
port: 587
useTls: true
defaultFrom: "tandoor@example.com"
existingSecret: "tandoor-email-secret"
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.existingSecret` | Existing secret for email | `""` |
| `config.email.passwordKey` | Key for password in secret | `email-password` |
### S3 Storage Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.s3.enabled` | Enable S3 storage | `false` |
| `config.s3.accessKey` | S3 access key | `""` |
| `config.s3.secretAccessKey` | S3 secret access key | `""` |
| `config.s3.bucketName` | S3 bucket name | `""` |
| `config.s3.regionName` | S3 region | `""` |
| `config.s3.endpointUrl` | Custom S3 endpoint (MinIO) | `""` |
| `config.s3.customDomain` | CDN/proxy domain | `""` |
| `config.s3.querystringAuth` | Use signed URLs | `true` |
| `config.s3.querystringExpire` | Signed URL expiration (seconds) | `3600` |
| `config.s3.existingSecret` | Existing secret for S3 | `""` |
### Social Authentication
| 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.creditsMonthly` | Monthly credits per space | `100` |
| `config.ai.rateLimit` | AI rate limit | `60/hour` |
### External Services
| Name | Description | Default |
|------|-------------|---------|
| `config.fdcApiKey` | Food Data Central API key | `DEMO_KEY` |
| `config.disableExternalConnectors` | Disable external connectors | `false` |
| `config.externalConnectorsQueueSize` | External connectors queue size | `100` |
### Rate Limiting
| Name | Description | Default |
|------|-------------|---------|
| `config.ratelimitUrlImportRequests` | Rate limit for URL imports | `""` |
| `config.drfThrottleRecipeUrlImport` | DRF throttle for recipe URL import | `60/hour` |
### Space & User Defaults
| Name | Description | Default |
|------|-------------|---------|
| `config.spaceDefaultMaxRecipes` | Max recipes 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.spaceDefaultAllowSharing` | Allow public sharing | `true` |
| `config.fractionPrefDefault` | Default fraction display | `false` |
| `config.commentPrefDefault` | Comments enabled by default | `true` |
| `config.stickyNavPrefDefault` | Sticky navbar by default | `true` |
| `config.maxOwnedSpacesPrefDefault` | Max spaces per user | `100` |
### Performance & Cosmetic
| 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.forceThemeFromSpace` | Space ID to enforce theme globally | `0` |
### Legal URLs
| Name | Description | Default |
|------|-------------|---------|
| `config.termsUrl` | Terms of service URL | `""` |
| `config.privacyUrl` | Privacy policy URL | `""` |
| `config.imprintUrl` | Legal imprint URL | `""` |
### hCaptcha Configuration
| Name | Description | Default |
|------|-------------|---------|
| `config.hcaptcha.siteKey` | hCaptcha site key | `""` |
| `config.hcaptcha.secret` | hCaptcha secret | `""` |
| `config.hcaptcha.existingSecret` | Existing secret for hCaptcha | `""` |
### Persistence Parameters
| Name | Description | Default |
|------|-------------|---------|
| `persistence.staticfiles.enabled` | Enable static files PVC | `true` |
| `persistence.staticfiles.existingClaim` | Existing PVC for static files | `""` |
| `persistence.staticfiles.storageClass` | Storage class | `""` |
| `persistence.staticfiles.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.staticfiles.size` | PVC size | `1Gi` |
| `persistence.mediafiles.enabled` | Enable media files PVC | `true` |
| `persistence.mediafiles.existingClaim` | Existing PVC for media files | `""` |
| `persistence.mediafiles.storageClass` | Storage class | `""` |
| `persistence.mediafiles.accessMode` | Access mode | `ReadWriteOnce` |
| `persistence.mediafiles.size` | PVC size | `5Gi` |
### 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 | `15` |
| `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` |
### Debugging
| Name | Description | Default |
|------|-------------|---------|
| `config.debug` | Enable Django debug mode | `false` |
| `config.debugToolbar` | Enable Debug Toolbar | `false` |
| `config.sqlDebug` | Enable SQL debug | `false` |
| `config.logLevel` | Application log level | `WARNING` |
| `config.gunicornLogLevel` | Gunicorn log level | `info` |
### Additional Configuration
| Name | Description | Default |
|------|-------------|---------|
| `env` | Extra environment variables | `[]` |
| `extraEnvFrom` | Extra env from secrets/configmaps | `[]` |
| `extraVolumes` | Extra volumes | `[]` |
| `extraVolumeMounts` | Extra volume mounts | `[]` |
## 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
kubectl logs -f deployment/tandoor
kubectl describe pod -l app.kubernetes.io/name=tandoor
```
## Links
- [Tandoor Recipes GitHub](https://github.com/TandoorRecipes/recipes)
- [Tandoor Documentation](https://docs.tandoor.dev/)
- [Configuration Reference](https://docs.tandoor.dev/system/configuration/)
- [Chart Source](https://github.com/rtomik/helm-charts/tree/main/charts/tandoor)

View File

@ -0,0 +1,43 @@
Tandoor Recipes has been deployed successfully!
{{- if .Values.ingress.enabled }}
Access Tandoor at:
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
Get the application URL by running:
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "tandoor.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
Get the application URL by running:
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "tandoor.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
Access Tandoor by port-forwarding:
kubectl --namespace {{ .Release.Namespace }} port-forward service/{{ include "tandoor.fullname" . }} 8080:{{ .Values.service.port }}
Then visit: http://localhost:8080
{{- end }}
IMPORTANT: This chart requires an external PostgreSQL database.
Make sure your PostgreSQL database is configured and accessible at:
Host: {{ .Values.postgresql.host }}
Port: {{ .Values.postgresql.port }}
Database: {{ .Values.postgresql.database }}
For more information, visit:
- Tandoor Documentation: https://docs.tandoor.dev/
- Configuration Reference: https://docs.tandoor.dev/system/configuration/

View File

@ -0,0 +1,59 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "tandoor.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "tandoor.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "tandoor.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "tandoor.labels" -}}
helm.sh/chart: {{ include "tandoor.chart" . }}
{{ include "tandoor.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "tandoor.selectorLabels" -}}
app.kubernetes.io/name: {{ include "tandoor.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
PostgreSQL host
*/}}
{{- define "tandoor.postgresql.host" -}}
{{- .Values.postgresql.host }}
{{- end }}
{{/*
PostgreSQL port
*/}}
{{- define "tandoor.postgresql.port" -}}
{{- .Values.postgresql.port | toString }}
{{- end }}

View File

@ -0,0 +1,402 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "tandoor.fullname" . }}
labels:
{{- include "tandoor.labels" . | nindent 4 }}
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
{{- include "tandoor.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
{{- include "tandoor.selectorLabels" . | nindent 8 }}
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.config.tandoorPort }}
protocol: TCP
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
successThreshold: {{ .Values.probes.liveness.successThreshold }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
successThreshold: {{ .Values.probes.readiness.successThreshold }}
{{- end }}
env:
# Database configuration
- name: DB_ENGINE
value: "django.db.backends.postgresql"
- name: POSTGRES_HOST
value: {{ include "tandoor.postgresql.host" . | quote }}
- name: POSTGRES_PORT
value: {{ include "tandoor.postgresql.port" . | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgresql.database | quote }}
- name: POSTGRES_USER
value: {{ .Values.postgresql.username | quote }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.existingSecret | default (printf "%s-secrets" (include "tandoor.fullname" .)) }}
key: {{ .Values.postgresql.passwordKey | default "postgresql-password" }}
# Security
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.config.secretKey.existingSecret | default (printf "%s-secrets" (include "tandoor.fullname" .)) }}
key: {{ .Values.config.secretKey.secretKey | default "secret-key" }}
- name: ALLOWED_HOSTS
value: {{ .Values.config.allowedHosts | quote }}
{{- if .Values.config.csrfTrustedOrigins }}
- name: CSRF_TRUSTED_ORIGINS
value: {{ .Values.config.csrfTrustedOrigins | quote }}
{{- end }}
- name: CORS_ALLOW_ALL_ORIGINS
value: {{ ternary "1" "0" .Values.config.corsAllowOrigins | quote }}
# Server configuration
- name: TANDOOR_PORT
value: {{ .Values.config.tandoorPort | quote }}
- name: GUNICORN_WORKERS
value: {{ .Values.config.gunicornWorkers | quote }}
- name: GUNICORN_THREADS
value: {{ .Values.config.gunicornThreads | quote }}
- name: GUNICORN_TIMEOUT
value: {{ .Values.config.gunicornTimeout | quote }}
- name: GUNICORN_MEDIA
value: {{ .Values.config.gunicornMedia | quote }}
# URL configuration
{{- if .Values.config.scriptName }}
- name: SCRIPT_NAME
value: {{ .Values.config.scriptName | quote }}
{{- end }}
# Session cookie configuration
{{- if .Values.config.sessionCookieDomain }}
- name: SESSION_COOKIE_DOMAIN
value: {{ .Values.config.sessionCookieDomain | quote }}
{{- end }}
- name: SESSION_COOKIE_NAME
value: {{ .Values.config.sessionCookieName | quote }}
# Time and locale
- name: TZ
value: {{ .Values.config.timezone | quote }}
# Feature toggles
- name: ENABLE_SIGNUP
value: {{ ternary "1" "0" .Values.config.enableSignup | quote }}
- name: ENABLE_METRICS
value: {{ ternary "1" "0" .Values.config.enableMetrics | quote }}
- name: ENABLE_PDF_EXPORT
value: {{ ternary "1" "0" .Values.config.enablePdfExport | quote }}
- name: SORT_TREE_BY_NAME
value: {{ ternary "1" "0" .Values.config.sortTreeByName | quote }}
# Social authentication
- name: SOCIAL_DEFAULT_ACCESS
value: {{ .Values.config.socialDefaultAccess | quote }}
- name: SOCIAL_DEFAULT_GROUP
value: {{ .Values.config.socialDefaultGroup | quote }}
{{- if or .Values.config.socialProviders .Values.config.oidc.enabled }}
- name: SOCIAL_PROVIDERS
value: {{ .Values.config.socialProviders | default "allauth.socialaccount.providers.openid_connect" | quote }}
{{- end }}
{{- if .Values.config.socialAccountProviders }}
- name: SOCIALACCOUNT_PROVIDERS
value: {{ .Values.config.socialAccountProviders | quote }}
{{- end }}
# OpenID Connect / OAuth configuration (e.g., Authentik, Keycloak)
# Note: For production, consider using extraEnvFrom with a secret containing SOCIALACCOUNT_PROVIDERS
{{- if .Values.config.oidc.enabled }}
{{- $clientId := .Values.config.oidc.clientId }}
{{- $clientSecret := .Values.config.oidc.clientSecret }}
- name: SOCIALACCOUNT_PROVIDERS
value: '{"openid_connect":{"APPS":[{"provider_id":"{{ .Values.config.oidc.providerId }}","name":"{{ .Values.config.oidc.providerName }}","client_id":"{{ $clientId }}","secret":"{{ $clientSecret }}","settings":{"server_url":"{{ .Values.config.oidc.serverUrl }}"}}]}}'
{{- end }}
# Remote user authentication
- name: REMOTE_USER_AUTH
value: {{ ternary "1" "0" .Values.config.remoteUserAuth | quote }}
# LDAP configuration
{{- if .Values.config.ldap.enabled }}
- name: LDAP_AUTH
value: "1"
- name: AUTH_LDAP_SERVER_URI
value: {{ .Values.config.ldap.serverUri | quote }}
{{- if .Values.config.ldap.bindDn }}
- name: AUTH_LDAP_BIND_DN
value: {{ .Values.config.ldap.bindDn | quote }}
{{- end }}
{{- if or .Values.config.ldap.bindPassword .Values.config.ldap.existingSecret }}
- name: AUTH_LDAP_BIND_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.config.ldap.existingSecret | default (printf "%s-secrets" (include "tandoor.fullname" .)) }}
key: {{ .Values.config.ldap.bindPasswordKey | default "ldap-bind-password" }}
{{- end }}
{{- if .Values.config.ldap.userSearchBaseDn }}
- name: AUTH_LDAP_USER_SEARCH_BASE_DN
value: {{ .Values.config.ldap.userSearchBaseDn | quote }}
{{- end }}
{{- if .Values.config.ldap.tlsCacertFile }}
- name: AUTH_LDAP_TLS_CACERTFILE
value: {{ .Values.config.ldap.tlsCacertFile | quote }}
{{- end }}
{{- if .Values.config.ldap.startTls }}
- name: AUTH_LDAP_START_TLS
value: "True"
{{- end }}
{{- end }}
# Email configuration
{{- if .Values.config.email.host }}
- name: EMAIL_HOST
value: {{ .Values.config.email.host | quote }}
- name: EMAIL_PORT
value: {{ .Values.config.email.port | quote }}
{{- if .Values.config.email.user }}
- name: EMAIL_HOST_USER
value: {{ .Values.config.email.user | quote }}
- name: EMAIL_HOST_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.config.email.existingSecret | default (printf "%s-secrets" (include "tandoor.fullname" .)) }}
key: {{ .Values.config.email.passwordKey | default "email-password" }}
{{- end }}
- name: EMAIL_USE_TLS
value: {{ ternary "1" "0" .Values.config.email.useTls | quote }}
- name: EMAIL_USE_SSL
value: {{ ternary "1" "0" .Values.config.email.useSsl | quote }}
- name: DEFAULT_FROM_EMAIL
value: {{ .Values.config.email.defaultFrom | quote }}
- name: ACCOUNT_EMAIL_SUBJECT_PREFIX
value: {{ .Values.config.email.accountEmailSubjectPrefix | quote }}
{{- end }}
# S3/Object storage configuration
{{- if .Values.config.s3.enabled }}
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.config.s3.existingSecret | default (printf "%s-secrets" (include "tandoor.fullname" .)) }}
key: {{ .Values.config.s3.accessKeyKey | default "s3-access-key" }}
- name: S3_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.config.s3.existingSecret | default (printf "%s-secrets" (include "tandoor.fullname" .)) }}
key: {{ .Values.config.s3.secretAccessKeyKey | default "s3-secret-access-key" }}
- name: S3_BUCKET_NAME
value: {{ .Values.config.s3.bucketName | quote }}
{{- if .Values.config.s3.regionName }}
- name: S3_REGION_NAME
value: {{ .Values.config.s3.regionName | quote }}
{{- end }}
{{- if .Values.config.s3.endpointUrl }}
- name: S3_ENDPOINT_URL
value: {{ .Values.config.s3.endpointUrl | quote }}
{{- end }}
{{- if .Values.config.s3.customDomain }}
- name: S3_CUSTOM_DOMAIN
value: {{ .Values.config.s3.customDomain | quote }}
{{- end }}
- name: S3_QUERYSTRING_AUTH
value: {{ ternary "1" "0" .Values.config.s3.querystringAuth | quote }}
- name: S3_QUERYSTRING_EXPIRE
value: {{ .Values.config.s3.querystringExpire | quote }}
{{- end }}
# AI features
{{- if .Values.config.ai.enabled }}
- name: SPACE_AI_ENABLED
value: "1"
- name: SPACE_AI_CREDITS_MONTHLY
value: {{ .Values.config.ai.creditsMonthly | quote }}
- name: AI_RATELIMIT
value: {{ .Values.config.ai.rateLimit | quote }}
{{- end }}
# Food Data Central API
- name: FDC_API_KEY
value: {{ .Values.config.fdcApiKey | quote }}
# External connectors
- name: DISABLE_EXTERNAL_CONNECTORS
value: {{ ternary "1" "0" .Values.config.disableExternalConnectors | quote }}
- name: EXTERNAL_CONNECTORS_QUEUE_SIZE
value: {{ .Values.config.externalConnectorsQueueSize | quote }}
# Rate limiting
{{- if .Values.config.ratelimitUrlImportRequests }}
- name: RATELIMIT_URL_IMPORT_REQUESTS
value: {{ .Values.config.ratelimitUrlImportRequests | quote }}
{{- end }}
- name: DRF_THROTTLE_RECIPE_URL_IMPORT
value: {{ .Values.config.drfThrottleRecipeUrlImport | quote }}
# Space defaults
- name: SPACE_DEFAULT_MAX_RECIPES
value: {{ .Values.config.spaceDefaultMaxRecipes | quote }}
- name: SPACE_DEFAULT_MAX_USERS
value: {{ .Values.config.spaceDefaultMaxUsers | quote }}
- name: SPACE_DEFAULT_MAX_FILES
value: {{ .Values.config.spaceDefaultMaxFiles | quote }}
- name: SPACE_DEFAULT_ALLOW_SHARING
value: {{ ternary "1" "0" .Values.config.spaceDefaultAllowSharing | quote }}
# User preference defaults
- name: FRACTION_PREF_DEFAULT
value: {{ ternary "1" "0" .Values.config.fractionPrefDefault | quote }}
- name: COMMENT_PREF_DEFAULT
value: {{ ternary "1" "0" .Values.config.commentPrefDefault | quote }}
- name: STICKY_NAV_PREF_DEFAULT
value: {{ ternary "1" "0" .Values.config.stickyNavPrefDefault | quote }}
- name: MAX_OWNED_SPACES_PREF_DEFAULT
value: {{ .Values.config.maxOwnedSpacesPrefDefault | quote }}
# Cosmetic
- name: UNAUTHENTICATED_THEME_FROM_SPACE
value: {{ .Values.config.unauthenticatedThemeFromSpace | quote }}
- name: FORCE_THEME_FROM_SPACE
value: {{ .Values.config.forceThemeFromSpace | quote }}
# Performance
- name: SHOPPING_MIN_AUTOSYNC_INTERVAL
value: {{ .Values.config.shoppingMinAutosyncInterval | quote }}
- name: EXPORT_FILE_CACHE_DURATION
value: {{ .Values.config.exportFileCacheDuration | quote }}
# Legal URLs
{{- if .Values.config.termsUrl }}
- name: TERMS_URL
value: {{ .Values.config.termsUrl | quote }}
{{- end }}
{{- if .Values.config.privacyUrl }}
- name: PRIVACY_URL
value: {{ .Values.config.privacyUrl | quote }}
{{- end }}
{{- if .Values.config.imprintUrl }}
- name: IMPRINT_URL
value: {{ .Values.config.imprintUrl | quote }}
{{- end }}
# hCaptcha
{{- if .Values.config.hcaptcha.siteKey }}
- name: HCAPTCHA_SITEKEY
value: {{ .Values.config.hcaptcha.siteKey | quote }}
- name: HCAPTCHA_SECRET
valueFrom:
secretKeyRef:
name: {{ .Values.config.hcaptcha.existingSecret | default (printf "%s-secrets" (include "tandoor.fullname" .)) }}
key: {{ .Values.config.hcaptcha.secretKeyKey | default "hcaptcha-secret" }}
{{- end }}
# Debugging
- name: DEBUG
value: {{ ternary "1" "0" .Values.config.debug | quote }}
- name: DEBUG_TOOLBAR
value: {{ ternary "1" "0" .Values.config.debugToolbar | quote }}
- name: SQL_DEBUG
value: {{ ternary "1" "0" .Values.config.sqlDebug | quote }}
- name: LOG_LEVEL
value: {{ .Values.config.logLevel | quote }}
- name: GUNICORN_LOG_LEVEL
value: {{ .Values.config.gunicornLogLevel | quote }}
# Custom environment variables
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- with .Values.extraEnvFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: staticfiles
mountPath: /opt/recipes/staticfiles
- name: mediafiles
mountPath: /opt/recipes/mediafiles
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
{{- if .Values.persistence.staticfiles.enabled }}
- name: staticfiles
persistentVolumeClaim:
claimName: {{ .Values.persistence.staticfiles.existingClaim | default (printf "%s-staticfiles" (include "tandoor.fullname" .)) }}
{{- else }}
- name: staticfiles
emptyDir: {}
{{- end }}
{{- if .Values.persistence.mediafiles.enabled }}
- name: mediafiles
persistentVolumeClaim:
claimName: {{ .Values.persistence.mediafiles.existingClaim | default (printf "%s-mediafiles" (include "tandoor.fullname" .)) }}
{{- else }}
- name: mediafiles
emptyDir: {}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "tandoor.fullname" . }}
labels:
{{- include "tandoor.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
{{- if .secretName }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "tandoor.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,44 @@
{{- if and .Values.persistence.staticfiles.enabled (not .Values.persistence.staticfiles.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "tandoor.fullname" . }}-staticfiles
labels:
{{- include "tandoor.labels" . | nindent 4 }}
{{- with .Values.persistence.staticfiles.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.staticfiles.accessMode | quote }}
{{- if .Values.persistence.staticfiles.storageClass }}
storageClassName: {{ .Values.persistence.staticfiles.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.staticfiles.size | quote }}
---
{{- end }}
{{- if and .Values.persistence.mediafiles.enabled (not .Values.persistence.mediafiles.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "tandoor.fullname" . }}-mediafiles
labels:
{{- include "tandoor.labels" . | nindent 4 }}
{{- with .Values.persistence.mediafiles.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.mediafiles.accessMode | quote }}
{{- if .Values.persistence.mediafiles.storageClass }}
storageClassName: {{ .Values.persistence.mediafiles.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.mediafiles.size | quote }}
{{- end }}

View File

@ -0,0 +1,49 @@
{{- $needsSecret := false -}}
{{- if not .Values.config.secretKey.existingSecret -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if not .Values.postgresql.existingSecret -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if and .Values.config.ldap.enabled .Values.config.ldap.bindPassword (not .Values.config.ldap.existingSecret) -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if and .Values.config.email.host .Values.config.email.user (not .Values.config.email.existingSecret) -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if and .Values.config.s3.enabled (not .Values.config.s3.existingSecret) -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if and .Values.config.hcaptcha.siteKey .Values.config.hcaptcha.secret (not .Values.config.hcaptcha.existingSecret) -}}
{{- $needsSecret = true -}}
{{- end -}}
{{- if $needsSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "tandoor.fullname" . }}-secrets
labels:
{{- include "tandoor.labels" . | nindent 4 }}
type: Opaque
data:
{{- if not .Values.config.secretKey.existingSecret }}
{{ .Values.config.secretKey.secretKey | default "secret-key" }}: {{ .Values.config.secretKey.value | default "change-me-tandoor-secret-key-at-least-50-characters-long-for-security" | b64enc }}
{{- end }}
{{- if not .Values.postgresql.existingSecret }}
{{ .Values.postgresql.passwordKey | default "postgresql-password" }}: {{ .Values.postgresql.password | default "tandoor" | b64enc }}
{{- end }}
{{- if and .Values.config.ldap.enabled .Values.config.ldap.bindPassword (not .Values.config.ldap.existingSecret) }}
{{ .Values.config.ldap.bindPasswordKey | default "ldap-bind-password" }}: {{ .Values.config.ldap.bindPassword | b64enc }}
{{- end }}
{{- if and .Values.config.email.host .Values.config.email.user (not .Values.config.email.existingSecret) }}
{{ .Values.config.email.passwordKey | default "email-password" }}: {{ .Values.config.email.password | default "" | b64enc }}
{{- end }}
{{- if and .Values.config.s3.enabled (not .Values.config.s3.existingSecret) }}
{{ .Values.config.s3.accessKeyKey | default "s3-access-key" }}: {{ .Values.config.s3.accessKey | default "" | b64enc }}
{{ .Values.config.s3.secretAccessKeyKey | default "s3-secret-access-key" }}: {{ .Values.config.s3.secretAccessKey | default "" | b64enc }}
{{- end }}
{{- if and .Values.config.hcaptcha.siteKey .Values.config.hcaptcha.secret (not .Values.config.hcaptcha.existingSecret) }}
{{ .Values.config.hcaptcha.secretKeyKey | default "hcaptcha-secret" }}: {{ .Values.config.hcaptcha.secret | b64enc }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "tandoor.fullname" . }}
labels:
{{- include "tandoor.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "tandoor.selectorLabels" . | nindent 4 }}

317
charts/tandoor/values.yaml Normal file
View File

@ -0,0 +1,317 @@
## Global settings
nameOverride: ""
fullnameOverride: ""
## Image settings
image:
repository: vabene1111/recipes
tag: "2.3.5"
pullPolicy: IfNotPresent
## Deployment settings
replicaCount: 1
revisionHistoryLimit: 3
# Pod security settings
# Note: Tandoor runs nginx internally which requires root privileges
# to write to /var/lib/nginx, /run/nginx, and /opt/recipes/http.d
podSecurityContext:
fsGroup: 0
containerSecurityContext:
runAsUser: 0
runAsGroup: 0
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
## Pod scheduling
nodeSelector: {}
tolerations: []
affinity: {}
## Service settings
service:
type: ClusterIP
port: 8080
## Ingress settings
ingress:
enabled: false
className: ""
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
# Enable these for proper HTTP to HTTPS redirect (prevents Origin: null issues)
# traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
hosts:
- host: tandoor.domain.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- tandoor.domain.com
# Optional: specify the name of an existing TLS secret
# secretName: "existing-tls-secret"
## Persistence settings
persistence:
# Tandoor static files directory
staticfiles:
enabled: true
# Use an existing PVC instead of creating a new one
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
annotations: {}
# Tandoor media files directory (recipe images, etc.)
mediafiles:
enabled: true
# Use an existing PVC instead of creating a new one
existingClaim: ""
storageClass: ""
accessMode: ReadWriteOnce
size: 5Gi
annotations: {}
# Extra volume mounts
extraVolumeMounts: []
# Extra volumes
extraVolumes: []
## Resource limits and requests
# resources:
# limits:
# cpu: 1000m
# memory: 512Mi
# requests:
# cpu: 100m
# memory: 256Mi
## Application health checks
probes:
liveness:
enabled: true
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
path: /
readiness:
enabled: true
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
path: /
## Autoscaling configuration
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
## External PostgreSQL database configuration
## This chart does NOT include PostgreSQL - you must provide an external database
postgresql:
host: "postgresql.default.svc.cluster.local"
port: 5432
database: "tandoor"
username: "tandoor"
# Use existingSecret for credentials (recommended for production)
existingSecret: ""
passwordKey: "postgresql-password"
# Or set password directly (not recommended for production)
password: ""
## Tandoor Configuration
## All settings based on official documentation: https://docs.tandoor.dev/system/configuration/
config:
# Required: Secret key for Django cryptographic operations (at least 50 characters)
secretKey:
# Use existingSecret for production
existingSecret: ""
secretKey: "secret-key"
# Or set directly (not recommended for production)
value: ""
# Security setting to prevent HTTP Host Header Attacks
allowedHosts: "*"
# Allows setting origins to allow for unsafe requests (CSRF)
csrfTrustedOrigins: ""
# Enable cross-origin resource sharing
corsAllowOrigins: false
# Time and locale settings
timezone: "UTC"
# Server configuration
tandoorPort: 8080
gunicornWorkers: 3
gunicornThreads: 2
gunicornTimeout: 30
gunicornMedia: 0
# URL configuration (for reverse proxy setups)
# URL path base for subfolder deployments
scriptName: ""
# Session cookie configuration
sessionCookieDomain: ""
sessionCookieName: "sessionid"
# Feature toggles
enableSignup: false
enableMetrics: false
enablePdfExport: false
sortTreeByName: false
# Social authentication
socialDefaultAccess: 0
socialDefaultGroup: "guest"
socialProviders: ""
# For OpenID Connect providers (like Authentik), use the socialAccountProviders field
# or set via env for complex JSON configurations
socialAccountProviders: ""
# OpenID Connect / OAuth configuration (e.g., Authentik, Keycloak, etc.)
# For simple single-provider OIDC setup, configure here.
# For complex multi-provider setups or production with secrets, use env + extraEnvFrom.
oidc:
enabled: false
# Provider ID (e.g., "authentik", "keycloak")
providerId: "authentik"
# Display name shown on login page
providerName: "Authentik"
# Client ID from your OIDC provider
clientId: ""
# Client Secret from your OIDC provider (for production, use extraEnvFrom with a secret)
clientSecret: ""
# OpenID Connect well-known configuration URL
# e.g., https://authentik.company/application/o/<application_slug>/.well-known/openid-configuration
serverUrl: ""
# Remote user authentication
remoteUserAuth: false
# LDAP authentication (optional)
ldap:
enabled: false
serverUri: ""
bindDn: ""
bindPassword: ""
bindPasswordFile: ""
userSearchBaseDn: ""
tlsCacertFile: ""
startTls: false
existingSecret: ""
bindPasswordKey: "ldap-bind-password"
# Email configuration (optional)
email:
host: ""
port: 25
user: ""
password: ""
useTls: false
useSsl: false
defaultFrom: "webmaster@localhost"
accountEmailSubjectPrefix: "[Tandoor Recipes]"
existingSecret: ""
passwordKey: "email-password"
# S3/Object storage configuration (optional)
s3:
enabled: false
accessKey: ""
secretAccessKey: ""
bucketName: ""
regionName: ""
endpointUrl: ""
customDomain: ""
querystringAuth: true
querystringExpire: 3600
existingSecret: ""
accessKeyKey: "s3-access-key"
secretAccessKeyKey: "s3-secret-access-key"
# AI features (optional)
ai:
enabled: false
creditsMonthly: 100
rateLimit: "60/hour"
# Food Data Central API key for nutrition data
fdcApiKey: "DEMO_KEY"
# External connectors
disableExternalConnectors: false
externalConnectorsQueueSize: 100
# Rate limiting
ratelimitUrlImportRequests: ""
drfThrottleRecipeUrlImport: "60/hour"
# Space defaults
spaceDefaultMaxRecipes: 0
spaceDefaultMaxUsers: 0
spaceDefaultMaxFiles: 0
spaceDefaultAllowSharing: true
# User preference defaults
fractionPrefDefault: false
commentPrefDefault: true
stickyNavPrefDefault: true
maxOwnedSpacesPrefDefault: 100
# Cosmetic
unauthenticatedThemeFromSpace: 0
forceThemeFromSpace: 0
# Performance
shoppingMinAutosyncInterval: 5
exportFileCacheDuration: 600
# Legal URLs (optional)
termsUrl: ""
privacyUrl: ""
imprintUrl: ""
# hCaptcha (optional)
hcaptcha:
siteKey: ""
secret: ""
existingSecret: ""
secretKeyKey: "hcaptcha-secret"
# Debugging (not recommended for production)
debug: false
debugToolbar: false
sqlDebug: false
logLevel: "WARNING"
gunicornLogLevel: "info"
# Environment variables (for additional configuration not covered above)
# Use this for advanced configurations or settings not exposed in config section
env: []
# Example: Custom environment variable
# - name: CUSTOM_VAR
# value: "custom-value"
#
# Example: Complex SOCIALACCOUNT_PROVIDERS for multiple OIDC providers
# - name: SOCIAL_PROVIDERS
# value: "allauth.socialaccount.providers.openid_connect"
# - name: SOCIALACCOUNT_PROVIDERS
# value: '{"openid_connect":{"APPS":[{"provider_id":"authentik","name":"Authentik","client_id":"your-client-id","secret":"your-client-secret","settings":{"server_url":"https://authentik.company/application/o/tandoor/.well-known/openid-configuration"}}]}}'
# Extra environment variables from secrets (recommended for sensitive data)
extraEnvFrom: []
# - secretRef:
# name: tandoor-extra-secrets