Compare commits

..

7 Commits

30 changed files with 3198 additions and 132 deletions

View File

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

View File

@ -27,12 +27,192 @@ $ helm install donetick donetick-chart/donetick
> **Tip**: List all releases using `helm list`
## Configuration Examples
### Basic Installation with SQLite (Default)
```bash
helm install donetick donetick-chart/donetick
```
### Installation with External PostgreSQL
Create a values file for PostgreSQL configuration:
```yaml
# values-postgres.yaml
config:
database:
type: "postgres"
host: "postgresql.database.svc.cluster.local"
port: 5432
user: "donetick"
password: "your-secure-password"
name: "donetick"
migration: true
# Update JWT secret for production
jwt:
secret: "your-secure-jwt-secret-at-least-32-characters-long"
# Configure server settings
server:
cors_allow_origins:
- "https://your-domain.com"
- "http://localhost:5173"
# Enable features as needed
features:
notifications: true
realtime: true
oauth: false
# Enable ingress for external access
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: donetick.your-domain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: donetick-tls
hosts:
- donetick.your-domain.com
# Configure persistence
persistence:
enabled: true
storageClass: "fast-ssd"
size: "5Gi"
```
Install with PostgreSQL configuration:
```bash
helm install donetick donetick-chart/donetick -f values-postgres.yaml
```
### Production Installation with External Secrets
For production deployments, use Kubernetes secrets for sensitive data:
```yaml
# values-production.yaml
config:
database:
type: "postgres"
host: "postgresql.database.svc.cluster.local"
port: 5432
name: "donetick"
# Use existing secret for postgres credentials
database:
type: "postgres"
host: "postgresql.database.svc.cluster.local"
port: 5432
name: "donetick"
secrets:
existingSecret: "donetick-postgres-secret"
userKey: "username"
passwordKey: "password"
# Use existing secret for JWT
jwt:
existingSecret: "donetick-jwt-secret"
secretKey: "jwt-secret"
session_time: "168h"
max_refresh: "168h"
# OAuth2 configuration with secrets
oauth2:
existingSecret: "donetick-oauth-secret"
clientIdKey: "client-id"
clientSecretKey: "client-secret"
auth_url: "https://your-oauth-provider.com/auth"
token_url: "https://your-oauth-provider.com/token"
user_info_url: "https://your-oauth-provider.com/userinfo"
redirect_url: "https://donetick.your-domain.com/auth/callback"
# Production server settings
server:
cors_allow_origins:
- "https://donetick.your-domain.com"
rate_limit: 100
rate_period: "60s"
# Enable production features
features:
notifications: true
realtime: true
oauth: true
# Security context for production
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# Resource limits for production
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# Ingress with TLS
ingress:
enabled: true
className: "nginx"
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: donetick.your-domain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: donetick-tls
hosts:
- donetick.your-domain.com
```
Create the required secrets:
```bash
# Postgres secret
kubectl create secret generic donetick-postgres-secret \
--from-literal=username='donetick' \
--from-literal=password='your-secure-db-password'
# JWT secret
kubectl create secret generic donetick-jwt-secret \
--from-literal=jwt-secret='your-very-secure-jwt-secret-at-least-32-characters-long'
# OAuth secret (if using OAuth)
kubectl create secret generic donetick-oauth-secret \
--from-literal=client-id='your-oauth-client-id' \
--from-literal=client-secret='your-oauth-client-secret'
```
Install with production configuration:
```bash
helm install donetick donetick-chart/donetick -f values-production.yaml
```
## Uninstalling the Chart
To uninstall/delete the `donetick` deployment:
```bash
$ helm uninstall donetick
helm uninstall donetick
```
## Parameters
@ -49,7 +229,7 @@ $ helm uninstall donetick
| Name | Description | Value |
|-------------------------|--------------------------------------------------------------------------------------|--------------------|
| `image.repository` | Donetick image repository | `donetick/donetick` |
| `image.tag` | Donetick image tag | `latest` |
| `image.tag` | Donetick image tag | `v0.1.60` |
| `image.pullPolicy` | Donetick image pull policy | `IfNotPresent` |
| `imagePullSecrets` | Global Docker registry secret names as an array | `[]` |
@ -62,12 +242,9 @@ $ helm uninstall donetick
| `config.oauth2.existingSecret` | Name of existing secret for OAuth2 credentials | `""` |
| `config.oauth2.clientIdKey` | Key in the existing secret for OAuth2 client ID | `"client-id"` |
| `config.oauth2.clientSecretKey` | Key in the existing secret for OAuth2 client secret | `"client-secret"` |
| `config.database.existingSecret` | Name of existing secret for database credentials | `""` |
| `config.database.hostKey` | Key in the existing secret for database host | `"db-host"` |
| `config.database.portKey` | Key in the existing secret for database port | `"db-port"` |
| `config.database.userKey` | Key in the existing secret for database user | `"db-user"` |
| `config.database.passwordKey` | Key in the existing secret for database password | `"db-password"` |
| `config.database.nameKey` | Key in the existing secret for database name | `"db-name"` |
| `config.database.secrets.existingSecret` | Name of existing secret for postgres credentials | `""` |
| `config.database.secrets.userKey` | Key in the existing secret for postgres username | `"username"` |
| `config.database.secrets.passwordKey` | Key in the existing secret for postgres password | `"password"` |
### Deployment parameters
@ -90,7 +267,12 @@ $ helm uninstall donetick
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `2021` |
| `service.annotations` | Additional annotations for Service | `{}` |
| `service.nodePort` | Service HTTP node port (when applicable) | `""` |
### Pod Configuration
| Name | Description | Value |
|----------------------------|------------------------------------------------------|-------------|
| `podAnnotations` | Additional annotations for pods | `{}` |
### Ingress parameters
@ -108,7 +290,174 @@ $ helm uninstall donetick
| Name | Description | Value |
|-------------------------------|------------------------------------------------------|---------------|
| `persistence.enabled` | Enable persistence using PVC | `true` |
| `persistence.storageClass` | PVC Storage Class | `"longhorn"` |
| `persistence.enabled` | Enable persistence using PVC | `false` |
| `persistence.storageClass` | PVC Storage Class | `""` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` |
| `persistence.size` | PVC Size | `1Gi` |
### Health Checks
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `probes.startup.enabled` | Enable startup probe | `true` |
| `probes.startup.initialDelaySeconds` | Initial delay for startup probe | `10` |
| `probes.startup.periodSeconds` | Period for startup probe | `10` |
| `probes.startup.failureThreshold` | Failure threshold for startup probe | `30` |
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.initialDelaySeconds` | Initial delay for liveness probe | `30` |
| `probes.liveness.periodSeconds` | Period for liveness probe | `10` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.initialDelaySeconds` | Initial delay for readiness probe | `5` |
| `probes.readiness.periodSeconds` | Period for readiness probe | `5` |
### Application Configuration
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.name` | Application name | `selfhosted` |
| `config.is_done_tick_dot_com` | Enable donetick.com features | `false` |
| `config.is_user_creation_disabled` | Disable user registration | `false` |
### Real-time Configuration
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.realtime.max_connections` | Maximum WebSocket connections | `100` |
| `config.realtime.ping_interval` | WebSocket ping interval | `30s` |
| `config.realtime.pong_wait` | WebSocket pong wait timeout | `60s` |
| `config.realtime.write_wait` | WebSocket write timeout | `10s` |
| `config.realtime.max_message_size` | Maximum WebSocket message size | `512` |
### Database Configuration
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.database.type` | Database type (sqlite or postgres) | `sqlite` |
| `config.database.migration` | Enable database migrations | `true` |
| `config.database.host` | PostgreSQL host (when type=postgres) | `""` |
| `config.database.port` | PostgreSQL port (when type=postgres) | `5432` |
| `config.database.user` | PostgreSQL user (when type=postgres) | `""` |
| `config.database.password` | PostgreSQL password (when type=postgres) | `""` |
| `config.database.name` | PostgreSQL database name (when type=postgres) | `""` |
### JWT Configuration
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.jwt.secret` | JWT signing secret | `changeme-this-secret-should-be-at-least-32-characters-long` |
| `config.jwt.session_time` | JWT session duration | `168h` |
| `config.jwt.max_refresh` | JWT maximum refresh duration | `168h` |
### Server Configuration
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.server.port` | Server port | `2021` |
| `config.server.read_timeout` | Server read timeout | `10s` |
| `config.server.write_timeout` | Server write timeout | `10s` |
| `config.server.rate_period` | Rate limiting period | `60s` |
| `config.server.rate_limit` | Rate limiting requests per period | `300` |
| `config.server.serve_frontend` | Serve frontend files | `true` |
### Feature Flags
| Name | Description | Value |
|----------------------------------------|------------------------------------------------------|---------------|
| `config.features.notifications` | Enable notifications | `true` |
| `config.features.realtime` | Enable real-time features | `true` |
| `config.features.oauth` | Enable OAuth authentication | `false` |
## Database Setup
### PostgreSQL Requirements
When using PostgreSQL, ensure you have:
1. **Database Created**: Create a database for Donetick
```sql
CREATE DATABASE donetick;
CREATE USER donetick WITH PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE donetick TO donetick;
```
2. **Network Access**: Ensure Donetick can reach your PostgreSQL instance
3. **Proper Credentials**: Configure database credentials in values or secrets
### Database Migration
Donetick automatically runs database migrations on startup when `config.database.migration: true`. For production:
1. **Review Migrations**: Check what migrations will be applied
2. **Backup Database**: Always backup before running migrations
3. **Monitor Startup**: Watch pod logs during initial deployment
## Troubleshooting
### Common Issues
#### 1. Real-time Configuration Panic
**Error**: `Invalid real-time configuration: maxConnections must be positive, got 0`
**Solution**: Ensure the real-time configuration is properly set:
```yaml
config:
realtime:
max_connections: 100 # Must be > 0
```
#### 2. Database Connection Issues
**Error**: Database connection failures
**Solutions**:
- Verify PostgreSQL is running and accessible
- Check database credentials in secrets
- Ensure database name exists
- Verify network policies allow connection
#### 3. JWT Secret Issues
**Error**: JWT authentication failures
**Solution**: Ensure JWT secret is at least 32 characters:
```yaml
config:
jwt:
secret: "your-very-secure-jwt-secret-at-least-32-characters-long"
```
#### 4. CORS Issues
**Error**: Cross-origin request blocked
**Solution**: Configure CORS origins:
```yaml
config:
server:
cors_allow_origins:
- "https://your-domain.com"
- "http://localhost:5173"
```
### Debugging
Check application logs:
```bash
kubectl logs deployment/donetick -f
```
Check configuration:
```bash
kubectl get configmap donetick-configmap -o yaml
```
Verify secrets:
```bash
kubectl get secret donetick-secrets -o yaml
```
## Contributing
Please feel free to contribute by opening issues or pull requests at:
https://github.com/rtomik/helm-charts
## License
This Helm chart is licensed under the MIT License.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,460 @@
# 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 (optional) | `""` |
| `postgresql.external.portKey` | Key in the secret for port (optional) | `""` |
| `postgresql.external.databaseKey` | Key in the secret for database name (optional) | `""` |
### Joplin Server Configuration
#### Admin Settings
| Name | Description | Value |
|----------------------------------------|-----------------------------------------------|-----------|
| `joplin.admin.email` | First admin user email | `""` |
| `joplin.admin.password` | First admin user password | `""` |
| `joplin.admin.existingSecret` | Name of existing secret with admin credentials | `""` |
| `joplin.admin.emailKey` | Key in the secret for admin email | `admin-email` |
| `joplin.admin.passwordKey` | Key in the secret for admin password | `admin-password` |
#### Server Settings
| Name | Description | Value |
|-------------------------------------------|-----------------------------------------------|-----------|
| `joplin.server.maxRequestBodySize` | Maximum request body size | `200mb` |
| `joplin.server.sessionTimeout` | Session timeout in seconds | `86400` |
| `joplin.server.enableUserRegistration` | Enable/disable user registration | `false` |
| `joplin.server.enableSharing` | Enable/disable sharing | `true` |
| `joplin.server.enablePublicNotes` | Enable/disable public notes | `true` |
#### Storage Settings
| Name | Description | Value |
|-------------------------------------------|-----------------------------------------------|--------------|
| `joplin.storage.driver` | Storage driver (filesystem, s3, azure) | `filesystem` |
| `joplin.storage.filesystemPath` | Path for filesystem storage | `/var/lib/joplin` |
##### S3 Storage (Optional)
| Name | Description | Value |
|---------------------------------------------|---------------------------------------------|-----------|
| `joplin.storage.s3.bucket` | S3 bucket name | `""` |
| `joplin.storage.s3.region` | S3 region | `""` |
| `joplin.storage.s3.accessKeyId` | S3 access key ID | `""` |
| `joplin.storage.s3.secretAccessKey` | S3 secret access key | `""` |
| `joplin.storage.s3.endpoint` | S3 endpoint (for S3-compatible services) | `""` |
| `joplin.storage.s3.existingSecret` | Name of existing secret with S3 credentials | `""` |
| `joplin.storage.s3.accessKeyIdKey` | Key in the secret for access key ID | `access-key-id` |
| `joplin.storage.s3.secretAccessKeyKey` | Key in the secret for secret access key | `secret-access-key` |
#### Email Settings (Optional)
| Name | Description | Value |
|----------------------------------------|-----------------------------------------------|-----------|
| `joplin.email.enabled` | Enable email notifications | `false` |
| `joplin.email.host` | SMTP host | `""` |
| `joplin.email.port` | SMTP port | `587` |
| `joplin.email.username` | SMTP username | `""` |
| `joplin.email.password` | SMTP password | `""` |
| `joplin.email.fromEmail` | From email address | `""` |
| `joplin.email.fromName` | From name | `Joplin Server` |
| `joplin.email.secure` | Use TLS/SSL | `true` |
| `joplin.email.existingSecret` | Name of existing secret with email credentials | `""` |
| `joplin.email.usernameKey` | Key in the secret for SMTP username | `email-username` |
| `joplin.email.passwordKey` | Key in the secret for SMTP password | `email-password` |
#### Logging Settings
| Name | Description | Value |
|--------------------------------|--------------------------------------|-----------|
| `joplin.logging.level` | Log level (error, warn, info, debug) | `info` |
| `joplin.logging.target` | Log target (console, file) | `console` |
### Persistence settings (for filesystem storage)
| Name | Description | Value |
|-------------------------------|----------------------------------|-----------------|
| `persistence.enabled` | Enable persistence using PVC | `true` |
| `persistence.storageClass` | PVC Storage Class | `""` |
| `persistence.accessMode` | PVC Access Mode | `ReadWriteOnce` |
| `persistence.size` | PVC Size | `10Gi` |
| `persistence.annotations` | Annotations for PVC | `{}` |
### Transcribe Service (Optional AI Transcription)
| Name | Description | Value |
|-------------------------------------------|-----------------------------------------------|--------------|
| `transcribe.enabled` | Enable transcribe service | `false` |
| `transcribe.image.repository` | Transcribe image repository | `joplin/transcribe` |
| `transcribe.image.tag` | Transcribe image tag | `latest` |
| `transcribe.image.pullPolicy` | Transcribe image pull policy | `IfNotPresent` |
| `transcribe.api.key` | Shared secret between Joplin and Transcribe | `""` |
| `transcribe.api.existingSecret` | Name of existing secret with transcribe API key | `""` |
| `transcribe.api.keyName` | Key in the secret for transcribe API key | `transcribe-api-key` |
| `transcribe.service.type` | Transcribe service type | `ClusterIP` |
| `transcribe.service.port` | Transcribe service port | `4567` |
| `transcribe.htr.imagesFolder` | HTR images folder path | `/app/images` |
#### Transcribe Database (Separate from main database)
| Name | Description | Value |
|---------------------------------------------|---------------------------------------------|-------------|
| `transcribe.database.host` | Transcribe database host | `""` |
| `transcribe.database.port` | Transcribe database port | `5432` |
| `transcribe.database.database` | Transcribe database name | `transcribe` |
| `transcribe.database.user` | Transcribe database username | `transcribe` |
| `transcribe.database.password` | Transcribe database password | `""` |
| `transcribe.database.existingSecret` | Name of existing secret with transcribe DB credentials | `""` |
| `transcribe.database.userKey` | Key in the secret for username | `username` |
| `transcribe.database.passwordKey` | Key in the secret for password | `password` |
### Resource Configuration
| Name | Description | Value |
|-------------|--------------------------------------|-------|
| `resources` | Resource limits and requests | `{}` |
### Health Checks
| Name | Description | Value |
|-------------------------------------------|------------------------------------------|-------|
| `probes.liveness.enabled` | Enable liveness probe | `true` |
| `probes.liveness.initialDelaySeconds` | Initial delay for liveness probe | `60` |
| `probes.liveness.periodSeconds` | Period for liveness probe | `30` |
| `probes.liveness.timeoutSeconds` | Timeout for liveness probe | `10` |
| `probes.liveness.failureThreshold` | Failure threshold for liveness probe | `3` |
| `probes.liveness.successThreshold` | Success threshold for liveness probe | `1` |
| `probes.liveness.path` | Path for liveness probe | `/api/ping` |
| `probes.liveness.httpHeaders` | HTTP headers for liveness probe | `[{"name": "Host", "value": "joplin.domain.com"}]` |
| `probes.readiness.enabled` | Enable readiness probe | `true` |
| `probes.readiness.initialDelaySeconds` | Initial delay for readiness probe | `30` |
| `probes.readiness.periodSeconds` | Period for readiness probe | `10` |
| `probes.readiness.timeoutSeconds` | Timeout for readiness probe | `5` |
| `probes.readiness.failureThreshold` | Failure threshold for readiness probe | `3` |
| `probes.readiness.successThreshold` | Success threshold for readiness probe | `1` |
| `probes.readiness.path` | Path for readiness probe | `/api/ping` |
| `probes.readiness.httpHeaders` | HTTP headers for readiness probe | `[{"name": "Host", "value": "joplin.domain.com"}]` |
### Autoscaling
| Name | Description | Value |
|---------------------------------------------|------------------------------------------|---------|
| `autoscaling.enabled` | Enable horizontal pod autoscaling | `false` |
| `autoscaling.minReplicas` | Minimum number of replicas | `1` |
| `autoscaling.maxReplicas` | Maximum number of replicas | `3` |
| `autoscaling.targetCPUUtilizationPercentage`| Target CPU utilization percentage | `80` |
| `autoscaling.targetMemoryUtilizationPercentage`| Target memory utilization percentage | `80` |
### Security Settings
| Name | Description | Value |
|-----------------------------------|--------------------------------------|---------|
| `security.httpsRedirect` | Enable/disable HTTPS redirect | `false` |
| `security.tls.enabled` | Enable custom TLS certificate | `false` |
| `security.tls.existingSecret` | Name of existing secret with TLS cert| `""` |
| `security.tls.certificateKey` | Key in the secret for TLS certificate| `tls.crt` |
| `security.tls.privateKeyKey` | Key in the secret for TLS private key| `tls.key` |
## Configuration Examples
### 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
#### Full Secret Configuration
```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"
```
#### Mixed Configuration (Host in values, credentials in secret)
```yaml
postgresql:
external:
enabled: true
host: "postgres-cluster-pooler.dbs.svc.cluster.local"
port: 5432
database: "joplin-server"
existingSecret: "joplin-db-credentials"
userKey: "username"
passwordKey: "password"
# hostKey, portKey, databaseKey left empty - using values above
```
### 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 and .Values.postgresql.external.existingSecret .Values.postgresql.external.hostKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.hostKey }}
{{- else }}
value: {{ .Values.postgresql.external.host | quote }}
{{- end }}
- name: POSTGRES_PORT
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.portKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.portKey }}
{{- else }}
value: {{ .Values.postgresql.external.port | quote }}
{{- end }}
- name: POSTGRES_DATABASE
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.databaseKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.databaseKey }}
{{- else }}
value: {{ .Values.postgresql.external.database | quote }}
{{- end }}
- name: POSTGRES_USER
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.userKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.userKey }}
{{- else }}
value: {{ .Values.postgresql.external.user | quote }}
{{- end }}
- name: POSTGRES_PASSWORD
{{- if and .Values.postgresql.external.existingSecret .Values.postgresql.external.passwordKey }}
valueFrom:
secretKeyRef:
name: {{ .Values.postgresql.external.existingSecret }}
key: {{ .Values.postgresql.external.passwordKey }}
{{- else }}
value: {{ .Values.postgresql.external.password | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.admin.email }}
- name: ADMIN_EMAIL
{{- if .Values.joplin.admin.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.admin.existingSecret }}
key: {{ .Values.joplin.admin.emailKey }}
{{- else }}
value: {{ .Values.joplin.admin.email | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.admin.password }}
- name: ADMIN_PASSWORD
{{- if .Values.joplin.admin.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.admin.existingSecret }}
key: {{ .Values.joplin.admin.passwordKey }}
{{- else }}
value: {{ .Values.joplin.admin.password | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.server.maxRequestBodySize }}
- name: MAX_REQUEST_BODY_SIZE
value: {{ .Values.joplin.server.maxRequestBodySize | quote }}
{{- end }}
{{- if .Values.joplin.server.sessionTimeout }}
- name: SESSION_TIMEOUT
value: {{ .Values.joplin.server.sessionTimeout | quote }}
{{- end }}
- name: ENABLE_USER_REGISTRATION
value: {{ .Values.joplin.server.enableUserRegistration | quote }}
- name: ENABLE_SHARING
value: {{ .Values.joplin.server.enableSharing | quote }}
- name: ENABLE_PUBLIC_NOTES
value: {{ .Values.joplin.server.enablePublicNotes | quote }}
{{- if eq .Values.joplin.storage.driver "filesystem" }}
- name: STORAGE_CONNECTION_STRING
value: "Type=Filesystem; Path={{ .Values.joplin.storage.filesystemPath }}"
{{- else if eq .Values.joplin.storage.driver "s3" }}
- name: STORAGE_CONNECTION_STRING
value: "Type=S3; Bucket={{ .Values.joplin.storage.s3.bucket }}; Region={{ .Values.joplin.storage.s3.region }}{{- if .Values.joplin.storage.s3.endpoint }}; Endpoint={{ .Values.joplin.storage.s3.endpoint }}{{- end }}"
{{- else }}
- name: STORAGE_CONNECTION_STRING
value: "Type=Database"
{{- end }}
{{- if and (eq .Values.joplin.storage.driver "s3") .Values.joplin.storage.s3.bucket }}
- name: AWS_ACCESS_KEY_ID
{{- if .Values.joplin.storage.s3.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.storage.s3.existingSecret }}
key: {{ .Values.joplin.storage.s3.accessKeyIdKey }}
{{- else }}
value: {{ .Values.joplin.storage.s3.accessKeyId | quote }}
{{- end }}
- name: AWS_SECRET_ACCESS_KEY
{{- if .Values.joplin.storage.s3.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.storage.s3.existingSecret }}
key: {{ .Values.joplin.storage.s3.secretAccessKeyKey }}
{{- else }}
value: {{ .Values.joplin.storage.s3.secretAccessKey | quote }}
{{- end }}
{{- end }}
{{- if .Values.joplin.email.enabled }}
- name: SMTP_HOST
value: {{ .Values.joplin.email.host | quote }}
- name: SMTP_PORT
value: {{ .Values.joplin.email.port | quote }}
- name: SMTP_USERNAME
{{- if .Values.joplin.email.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.email.existingSecret }}
key: {{ .Values.joplin.email.usernameKey }}
{{- else }}
value: {{ .Values.joplin.email.username | quote }}
{{- end }}
- name: SMTP_PASSWORD
{{- if .Values.joplin.email.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.joplin.email.existingSecret }}
key: {{ .Values.joplin.email.passwordKey }}
{{- else }}
value: {{ .Values.joplin.email.password | quote }}
{{- end }}
- name: SMTP_FROM_EMAIL
value: {{ .Values.joplin.email.fromEmail | quote }}
- name: SMTP_FROM_NAME
value: {{ .Values.joplin.email.fromName | quote }}
- name: SMTP_SECURE
value: {{ .Values.joplin.email.secure | quote }}
{{- end }}
{{- if .Values.transcribe.enabled }}
- name: TRANSCRIBE_ENABLED
value: "true"
- name: TRANSCRIBE_API_KEY
{{- if .Values.transcribe.api.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.api.existingSecret }}
key: {{ .Values.transcribe.api.keyName }}
{{- else }}
value: {{ .Values.transcribe.api.key | quote }}
{{- end }}
- name: TRANSCRIBE_BASE_URL
value: "http://{{ include "joplin-server.fullname" . }}-transcribe:{{ .Values.transcribe.service.port }}"
{{- end }}
- name: LOG_LEVEL
value: {{ .Values.joplin.logging.level | quote }}
- name: LOG_TARGET
value: {{ .Values.joplin.logging.target | quote }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
{{- if eq .Values.joplin.storage.driver "filesystem" }}
- name: data
mountPath: {{ .Values.joplin.storage.filesystemPath }}
{{- end }}
{{- if and .Values.security.tls.enabled .Values.security.tls.existingSecret }}
- name: tls-certs
mountPath: /app/certs
readOnly: true
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.transcribe.enabled }}
- name: transcribe
image: "{{ .Values.transcribe.image.repository }}:{{ .Values.transcribe.image.tag }}"
imagePullPolicy: {{ .Values.transcribe.image.pullPolicy }}
ports:
- name: transcribe
containerPort: 4567
protocol: TCP
env:
- name: APP_PORT
value: "4567"
- name: DB_CLIENT
value: "pg"
- name: API_KEY
{{- if .Values.transcribe.api.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.api.existingSecret }}
key: {{ .Values.transcribe.api.keyName }}
{{- else }}
value: {{ .Values.transcribe.api.key | quote }}
{{- end }}
- name: HTR_CLI_IMAGES_FOLDER
value: {{ .Values.transcribe.htr.imagesFolder | quote }}
{{- if .Values.transcribe.database.host }}
- name: QUEUE_DATABASE_HOST
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.hostKey }}
{{- else }}
value: {{ .Values.transcribe.database.host | quote }}
{{- end }}
- name: QUEUE_DATABASE_PORT
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.portKey }}
{{- else }}
value: {{ .Values.transcribe.database.port | quote }}
{{- end }}
- name: QUEUE_DATABASE_NAME
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.databaseKey }}
{{- else }}
value: {{ .Values.transcribe.database.database | quote }}
{{- end }}
- name: QUEUE_DATABASE_USER
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.userKey }}
{{- else }}
value: {{ .Values.transcribe.database.user | quote }}
{{- end }}
- name: QUEUE_DATABASE_PASSWORD
{{- if .Values.transcribe.database.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.transcribe.database.existingSecret }}
key: {{ .Values.transcribe.database.passwordKey }}
{{- else }}
value: {{ .Values.transcribe.database.password | quote }}
{{- end }}
{{- end }}
volumeMounts:
- name: transcribe-images
mountPath: {{ .Values.transcribe.htr.imagesFolder }}
- name: docker-socket
mountPath: /var/run/docker.sock
{{- end }}
volumes:
{{- if eq .Values.joplin.storage.driver "filesystem" }}
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "joplin-server.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}
{{- if .Values.transcribe.enabled }}
- name: transcribe-images
{{- if .Values.transcribe.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "joplin-server.fullname" . }}-transcribe-images
{{- else }}
emptyDir: {}
{{- end }}
- name: docker-socket
hostPath:
path: /var/run/docker.sock
type: Socket
{{- end }}
{{- if and .Values.security.tls.enabled .Values.security.tls.existingSecret }}
- name: tls-certs
secret:
secretName: {{ .Values.security.tls.existingSecret }}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ 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"
version: 0.0.2
appVersion: "v3.2.1"
maintainers:
- name: Richard Tomik
email: no@m.com

View File

@ -5,7 +5,7 @@ fullnameOverride: ""
## Image settings
image:
repository: ghcr.io/mealie-recipes/mealie
tag: "v3.1.1"
tag: "v3.2.1"
pullPolicy: IfNotPresent
## Deployment settings

View File

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

View File

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

View File

@ -0,0 +1,238 @@
# Paperless-ngx Helm Chart
A Helm chart for deploying Paperless-ngx document management system on Kubernetes.
## Introduction
This chart deploys [Paperless-ngx](https://github.com/paperless-ngx/paperless-ngx) on a Kubernetes cluster using the Helm package manager.
Paperless-ngx is a community-supported supercharged version of paperless: scan, index and archive all your physical documents.
Source code can be found here:
- https://github.com/rtomik/helm-charts/tree/main/charts/paperless-ngx
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- PV provisioner support in the underlying infrastructure
- **External PostgreSQL database** (required)
- **External Redis server** (required)
## External Dependencies
This chart requires external PostgreSQL and Redis services. It does not deploy these dependencies to avoid resource conflicts on centralized servers.
### PostgreSQL Setup
Paperless-ngx requires PostgreSQL 11+ as its database backend. Ensure you have:
- A PostgreSQL database created for Paperless-ngx
- Database credentials configured in values.yaml or via secrets
### Redis Setup
Redis is required for background task processing. Ensure you have:
- A Redis server accessible from the cluster
- Connection details configured in values.yaml
## Installing the Chart
To install the chart with the release name `paperless-ngx`:
```bash
$ helm repo add paperless-chart https://rtomik.github.io/helm-charts
$ helm install paperless-ngx paperless-chart/paperless-ngx
```
Or install directly from this repository:
```bash
$ git clone https://github.com/rtomik/helm-charts.git
$ cd helm-charts/charts/paperless-ngx
$ helm install paperless-ngx .
```
> **Tip**: List all releases using `helm list`
## Configuration
The following table lists the configurable parameters and their default values.
### Global Parameters
| Name | Description | Value |
|------------------------|-------------------------------------------------------------------------------------|-------|
| `nameOverride` | String to partially override the release name | `""` |
| `fullnameOverride` | String to fully override the release name | `""` |
### Image Parameters
| Name | Description | Value |
|-------------------------|--------------------------------------------------------------------------------------|--------------------|
| `image.repository` | Paperless-ngx image repository | `ghcr.io/paperless-ngx/paperless-ngx` |
| `image.tag` | Paperless-ngx image tag | `latest` |
| `image.pullPolicy` | Paperless-ngx image pull policy | `IfNotPresent` |
### External Dependencies
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|-------------------------------------------|
| `postgresql.external.enabled` | Enable external PostgreSQL configuration | `true` |
| `postgresql.external.host` | External PostgreSQL host | `postgresql.default.svc.cluster.local` |
| `postgresql.external.port` | External PostgreSQL port | `5432` |
| `postgresql.external.database` | External PostgreSQL database name | `paperless` |
| `postgresql.external.username` | External PostgreSQL username | `paperless` |
| `postgresql.external.existingSecret` | Existing secret with PostgreSQL credentials | `""` |
| `postgresql.external.passwordKey` | Key in existing secret for PostgreSQL password | `postgresql-password` |
| `redis.external.enabled` | Enable external Redis configuration | `true` |
| `redis.external.host` | External Redis host | `redis.default.svc.cluster.local` |
| `redis.external.port` | External Redis port | `6379` |
| `redis.external.database` | External Redis database number | `0` |
### Security Configuration
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|---------------------|
| `config.secretKey.existingSecret` | Name of existing secret for Django secret key | `""` |
| `config.secretKey.secretKey` | Key in the existing secret for Django secret key | `secret-key` |
| `config.admin.user` | Admin username to create on startup | `""` |
| `config.admin.password` | Admin password (use existingSecret for production) | `""` |
| `config.admin.email` | Admin email address | `root@localhost` |
| `config.admin.existingSecret` | Name of existing secret for admin credentials | `""` |
### Application Configuration
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|---------------------|
| `config.url` | External URL for Paperless-ngx (e.g., https://paperless.domain.com) | `""` |
| `config.allowedHosts` | Comma-separated list of allowed hosts | `*` |
| `config.timeZone` | Application timezone | `UTC` |
| `config.ocr.language` | OCR language (3-letter code) | `eng` |
| `config.ocr.mode` | OCR mode (skip, redo, force) | `skip` |
| `config.consumer.recursive` | Enable recursive consumption directory watching | `false` |
| `config.consumer.subdirsAsTags` | Use subdirectory names as tags | `false` |
### Persistence Parameters
| Name | Description | Value |
|----------------------------------------|--------------------------------------------------------------------|---------------------|
| `persistence.data.enabled` | Enable persistence for data directory | `true` |
| `persistence.data.size` | Size of data PVC | `1Gi` |
| `persistence.media.enabled` | Enable persistence for media directory | `true` |
| `persistence.media.size` | Size of media PVC | `10Gi` |
| `persistence.consume.enabled` | Enable persistence for consume directory | `true` |
| `persistence.consume.size` | Size of consume PVC | `5Gi` |
| `persistence.export.enabled` | Enable persistence for export directory | `true` |
| `persistence.export.size` | Size of export PVC | `1Gi` |
### Service Parameters
| Name | Description | Value |
|----------------------------|------------------------------------------------------|-------------|
| `service.type` | Kubernetes Service type | `ClusterIP` |
| `service.port` | Service HTTP port | `8000` |
### Ingress Parameters
| Name | Description | Value |
|----------------------------|------------------------------------------------------|----------------------|
| `ingress.enabled` | Enable ingress record generation | `false` |
| `ingress.className` | IngressClass name | `""` |
| `ingress.annotations` | Additional annotations for the Ingress resource | See values.yaml |
| `ingress.hosts` | Array of host and path objects | See values.yaml |
| `ingress.tls` | TLS configuration | See values.yaml |
## Usage Examples
### Basic Installation
```bash
helm install paperless-ngx . \
--set postgresql.external.host=my-postgres.example.com \
--set postgresql.external.password=secretpassword \
--set redis.external.host=my-redis.example.com
```
### Production Installation with External Secrets
```yaml
# values-production.yaml
config:
url: "https://paperless.example.com"
allowedHosts: "paperless.example.com"
secretKey:
existingSecret: "paperless-secrets"
secretKey: "django-secret-key"
admin:
user: "admin"
existingSecret: "paperless-admin-secrets"
postgresql:
external:
host: "postgresql.database.svc.cluster.local"
existingSecret: "paperless-db-secrets"
passwordKey: "password"
redis:
external:
host: "redis.cache.svc.cluster.local"
ingress:
enabled: true
className: "nginx"
hosts:
- host: paperless.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: paperless-tls
hosts:
- paperless.example.com
```
```bash
helm install paperless-ngx . -f values-production.yaml
```
## Security Considerations
1. **Use external secrets** for production deployments to store sensitive data like database passwords and the Django secret key.
2. **Set a proper PAPERLESS_URL** when exposing the application externally.
3. **Configure ALLOWED_HOSTS** to restrict which hosts can access the application.
4. **Use HTTPS** when exposing the application to the internet.
5. **Container Security**: The container runs as root initially to allow s6-overlay to set up the runtime environment, then drops privileges to UID 1000. This is required for the Paperless-ngx Docker image to function properly.
## Volumes and Data
Paperless-ngx uses several directories:
- **Data directory**: Contains the search index, classification model, and SQLite database (if used)
- **Media directory**: Contains all uploaded documents and thumbnails
- **Consume directory**: Drop documents here for automatic processing
- **Export directory**: Used for document exports
All directories can be configured with separate PVCs and storage classes.
## Uninstalling the Chart
To uninstall/delete the `paperless-ngx` deployment:
```bash
helm uninstall paperless-ngx
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
## Contributing
Please feel free to contribute by opening issues or pull requests at:
https://github.com/rtomik/helm-charts
## License
This Helm chart is licensed under the MIT License.
## Links
- [Paperless-ngx Documentation](https://docs.paperless-ngx.com/)
- [Paperless-ngx GitHub Repository](https://github.com/paperless-ngx/paperless-ngx)
- [Docker Hub](https://hub.docker.com/r/ghcr.io/paperless-ngx/paperless-ngx)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,57 +0,0 @@
ingress:
enabled: true
className: "traefik"
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: mealie.tomik.lat
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- mealie.tomik.lat
persistence:
enabled: true
storageClass: "longhorn"
accessMode: ReadWriteOnce
size: 3Gi
postgresql:
enabled: true
# External PostgreSQL settings
external:
enabled: true
host: "postgres-cluster-pooler.dbs.svc.cluster.local"
port: 5432
database: "mealie"
user: "mealie_user"
password: "7OemzeEtwYF1y7FyqRi6"
## 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"
# Webworker
UVICORN_WORKERS: "1"