Compare commits

...

3 Commits

Author SHA1 Message Date
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
28 changed files with 3264 additions and 0 deletions

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

View File

@ -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 .Values.postgresql.external.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.hostKey }}
{{- else }}
value: {{ .Values.postgresql.external.host | quote }}
{{- end }}
- name: POSTGRES_PORT
{{- if .Values.postgresql.external.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.portKey }}
{{- else }}
value: {{ .Values.postgresql.external.port | quote }}
{{- end }}
- name: POSTGRES_DATABASE
{{- if .Values.postgresql.external.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.databaseKey }}
{{- else }}
value: {{ .Values.postgresql.external.database | quote }}
{{- end }}
- 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.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,267 @@
## 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"
hostKey: "host"
portKey: "port"
databaseKey: "database"
## 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 }}

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

@ -0,0 +1,108 @@
# Karakeep Helm Chart
This Helm chart deploys [Karakeep](https://github.com/karakeep-app/karakeep), a bookmark management application, along with its required services on a Kubernetes cluster.
## Components
This chart deploys three containers in a single pod:
1. **Karakeep**: The main bookmark management application
2. **Chrome**: Headless Chrome browser for web scraping and preview generation
3. **MeiliSearch**: Search engine for fast bookmark search functionality
## Prerequisites
- Kubernetes 1.19+
- Helm 3.2.0+
- PV provisioner support in the underlying infrastructure (if persistence is enabled)
## Installing the Chart
To install the chart with the release name `karakeep`:
```bash
helm repo add karakeep-chart https://rtomik.github.io/helm-charts
helm install karakeep karakeep-chart/karakeep
```
## Uninstalling the Chart
To uninstall/delete the `karakeep` deployment:
```bash
helm delete karakeep
```
## Configuration
The following table lists the configurable parameters and their default values.
### Global Settings
| Parameter | Description | Default |
|-----------|-------------|---------|
| `nameOverride` | Override the name of the chart | `""` |
| `fullnameOverride` | Override the full name of the chart | `""` |
| `replicaCount` | Number of replicas | `1` |
### Karakeep Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `karakeep.image.repository` | Karakeep image repository | `ghcr.io/karakeep-app/karakeep` |
| `karakeep.image.tag` | Karakeep image tag | `"release"` |
| `karakeep.image.pullPolicy` | Image pull policy | `IfNotPresent` |
### Chrome Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `chrome.image.repository` | Chrome image repository | `gcr.io/zenika-hub/alpine-chrome` |
| `chrome.image.tag` | Chrome image tag | `"124"` |
### MeiliSearch Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `meilisearch.image.repository` | MeiliSearch image repository | `getmeili/meilisearch` |
| `meilisearch.image.tag` | MeiliSearch image tag | `"v1.13.3"` |
### Persistence
| Parameter | Description | Default |
|-----------|-------------|---------|
| `persistence.enabled` | Enable persistent storage | `true` |
| `persistence.data.size` | Size of data volume | `5Gi` |
| `persistence.data.storageClass` | Storage class for data volume | `""` |
| `persistence.meilisearch.size` | Size of MeiliSearch volume | `2Gi` |
| `persistence.meilisearch.storageClass` | Storage class for MeiliSearch volume | `""` |
### Ingress
| Parameter | Description | Default |
|-----------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.hosts[0].host` | Hostname | `karakeep.domain.com` |
### Secrets
| Parameter | Description | Default |
|-----------|-------------|---------|
| `secrets.create` | Create secret for environment variables | `false` |
| `secrets.existingSecret` | Use existing secret | `""` |
| `secrets.env` | Environment variables to store in secret | `{}` |
**Important Configuration:**
1. The default `NEXTAUTH_SECRET` is set to a placeholder value. For production deployments, you should either:
- Override the value: `--set karakeep.env[3].value="your-secure-32-character-string"`
- Use secrets: `--set secrets.create=true --set secrets.env.NEXTAUTH_SECRET="your-secure-32-character-string"`
2. When ingress is enabled, `NEXTAUTH_URL` is automatically set to the ingress hostname. For custom configurations:
- Override manually: `--set karakeep.env[4].value="https://your-domain.com"`
## Notes
- This chart creates a multi-container pod with all three services running together
- Data persistence is enabled by default with separate volumes for Karakeep data and MeiliSearch indices
- The services communicate via localhost since they share the same pod network
- Chrome runs with security flags for containerized environments

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.1
appVersion: "v3.1.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.

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

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