wave-1 ringtail: app manifests + ArgoCD apps (paperless, teslamate, mealie)

Staging deployments on ringtail k3s, in parallel with the minikube apps
until per-service cutover. Each uses the Nix image built at 1d4cbbf
(paperless v2.20.15, mealie v3.16.0, teslamate v3.0.0, all -nix tags) and
points postgres at the in-cluster ringtail blumeops-pg.

- paperless: redesigned as web/worker/beat/consumer + redis in one pod
  (Nix image has no s6 supervisor); media on a ringtail-suffixed NFS PV
  (needs a sifaka rule for ringtail).
- mealie: single gunicorn; SQLite PVC (local-path) copied at cutover.
- teslamate: stateless; DATABASE_HOST already in-cluster, unchanged.

ArgoCD apps target ringtail (https://ringtail.tail8d86e.ts.net:6443).
Not synced yet; deploy-from-branch + cutover is the next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-03 09:42:22 -07:00
commit 18dc9a143c
22 changed files with 759 additions and 0 deletions

View file

@ -0,0 +1,26 @@
# Mealie on ringtail k3s.
#
# Wave-1 indri-k8s decommission. Staging deployment; the minikube `mealie`
# app stays in parallel until cutover (copy SQLite PVC, drop the minikube
# tailscale ingress, flip Caddy). See [[migrate-wave1-ringtail]].
#
# Prerequisites:
# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore)
# - mealie-data PVC contents copied from minikube at cutover
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: mealie-ringtail
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/mealie-ringtail
destination:
server: https://ringtail.tail8d86e.ts.net:6443
namespace: mealie
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -0,0 +1,28 @@
# Paperless-ngx on ringtail k3s.
#
# Wave-1 indri-k8s decommission. Staging deployment; the minikube
# `paperless` app stays in parallel until cutover (drop the minikube
# tailscale ingress to free the name, then flip Caddy). See
# [[migrate-wave1-ringtail]].
#
# Prerequisites:
# - databases-ringtail blumeops-pg (paperless database + role)
# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore)
# - sifaka NFS rule granting ringtail access to /volume1/paperless
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: paperless-ringtail
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/paperless-ringtail
destination:
server: https://ringtail.tail8d86e.ts.net:6443
namespace: paperless
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -0,0 +1,28 @@
# TeslaMate on ringtail k3s.
#
# Wave-1 indri-k8s decommission. Staging deployment; the minikube
# `teslamate` app stays in parallel until cutover (migrate the teslamate
# database, drop the minikube tailscale ingress, flip Caddy). See
# [[migrate-wave1-ringtail]].
#
# Prerequisites:
# - databases-ringtail blumeops-pg (teslamate database + role; cube +
# earthdistance extensions created by superuser at cutover)
# - external-secrets-ringtail (onepassword-blumeops ClusterSecretStore)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: teslamate-ringtail
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/teslamate-ringtail
destination:
server: https://ringtail.tail8d86e.ts.net:6443
namespace: teslamate
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -0,0 +1,102 @@
# Mealie on ringtail k3s — Nix image.
#
# Single gunicorn process (the Nix image's default `mealie-run` entrypoint
# runs init_db then gunicorn), serving the prebuilt frontend. DB is SQLite
# on the mealie-data PVC; its contents are copied from the minikube PVC at
# cutover. See [[migrate-wave1-ringtail]].
apiVersion: apps/v1
kind: Deployment
metadata:
name: mealie
namespace: mealie
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: mealie
template:
metadata:
labels:
app: mealie
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: mealie
image: registry.ops.eblu.me/blumeops/mealie:kustomized
ports:
- containerPort: 9000
env:
- name: BASE_URL
value: "https://meals.ops.eblu.me"
- name: ALLOW_SIGNUP
value: "false"
- name: TZ
value: "America/Los_Angeles"
- name: MAX_WORKERS
value: "1"
- name: WEB_CONCURRENCY
value: "1"
# OIDC — Authentik (public client, PKCE)
- name: OIDC_AUTH_ENABLED
value: "true"
- name: OIDC_CONFIGURATION_URL
value: "https://authentik.ops.eblu.me/application/o/mealie/.well-known/openid-configuration"
- name: OIDC_CLIENT_ID
value: "mealie"
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: mealie-secrets
key: oidc-client-secret
- name: OIDC_AUTO_REDIRECT
value: "false"
- name: OIDC_PROVIDER_NAME
value: "Authentik"
- name: OIDC_ADMIN_GROUP
value: "admins"
- name: OIDC_SIGNUP_ENABLED
value: "true"
- name: OIDC_USER_CLAIM
value: "email"
# OpenAI — recipe parsing, image OCR, ingredient extraction
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: mealie-secrets
key: openai-api-key
- name: OPENAI_MODEL
value: "gpt-4o"
- name: OPENAI_REQUEST_TIMEOUT
value: "120"
- name: OPENAI_WORKERS
value: "1"
volumeMounts:
- name: data
mountPath: /app/data
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "1000Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/app/about
port: 9000
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/app/about
port: 9000
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: data
persistentVolumeClaim:
claimName: mealie-data

View file

@ -0,0 +1,23 @@
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mealie-secrets
namespace: mealie
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: mealie-secrets
creationPolicy: Owner
data:
- secretKey: oidc-client-secret
remoteRef:
key: "Authentik (blumeops)"
property: mealie-client-secret
- secretKey: openai-api-key
remoteRef:
key: "openai (blumeops)"
property: credential

View file

@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mealie-tailscale
namespace: mealie
annotations:
tailscale.com/proxy-class: "default"
tailscale.com/proxy-group: "ingress"
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Mealie"
gethomepage.dev/group: "Home"
gethomepage.dev/icon: "mealie.png"
gethomepage.dev/description: "Recipe manager"
gethomepage.dev/href: "https://meals.ops.eblu.me"
gethomepage.dev/pod-selector: "app=mealie"
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: mealie
port:
number: 9000
tls:
- hosts:
- meals

View file

@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: mealie
resources:
- deployment.yaml
- service.yaml
- pvc.yaml
- ingress-tailscale.yaml
- external-secret.yaml
images:
- name: registry.ops.eblu.me/blumeops/mealie
newTag: v3.16.0-1d4cbbf-nix

View file

@ -0,0 +1,14 @@
# SQLite data volume for Mealie on ringtail. Contents copied from the
# minikube mealie-data PVC at cutover (recipes, meal plans, uploaded media).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mealie-data
namespace: mealie
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 2Gi

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: mealie
namespace: mealie
spec:
selector:
app: mealie
ports:
- name: http
port: 9000
targetPort: 9000
protocol: TCP

View file

@ -0,0 +1,184 @@
# Paperless-ngx on ringtail k3s — Nix image, multi-process.
#
# The upstream s6 image ran web + worker + scheduler + consumer in one
# container. The Nix image (containers/paperless/default.nix) ships the
# binaries but no supervisor, so we run those as four containers in one
# pod, sharing the local data/consume dirs (emptyDir) and the NFS media
# volume; redis is colocated so PAPERLESS_REDIS=localhost works for all.
#
# DB now points in-cluster at the ringtail blumeops-pg (was pg.ops.eblu.me
# on indri). PAPERLESS_{DATA_DIR,MEDIA_ROOT,CONSUMPTION_DIR} are set
# explicitly because the Nix package does not default to the upstream
# /usr/src/paperless paths.
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless
namespace: paperless
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: paperless
template:
metadata:
labels:
app: paperless
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: registry.ops.eblu.me/blumeops/paperless:kustomized
ports:
- containerPort: 8000
name: http
env: &paperless-env
- name: PAPERLESS_URL
value: "https://paperless.ops.eblu.me"
- name: PAPERLESS_REDIS
value: "redis://localhost:6379"
- name: PAPERLESS_DBHOST
value: "blumeops-pg-rw.databases.svc.cluster.local"
- name: PAPERLESS_DBPORT
value: "5432"
- name: PAPERLESS_DBNAME
value: "paperless"
- name: PAPERLESS_DBUSER
value: "paperless"
- name: PAPERLESS_DBPASS
valueFrom:
secretKeyRef:
name: paperless-secrets
key: db-password
# Explicit port to override the k8s-injected PAPERLESS_PORT
# (service named 'paperless' would set PAPERLESS_PORT=tcp://...)
- name: PAPERLESS_PORT
value: "8000"
- 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"
- name: PAPERLESS_SECRET_KEY
valueFrom:
secretKeyRef:
name: paperless-secrets
key: secret-key
- name: PAPERLESS_TIME_ZONE
value: "America/Los_Angeles"
- name: PAPERLESS_OCR_LANGUAGE
value: "eng"
- name: PAPERLESS_TASK_WORKERS
value: "1"
- name: PAPERLESS_ADMIN_USER
value: "eblume"
- name: PAPERLESS_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: paperless-secrets
key: admin-password
- name: PAPERLESS_ADMIN_MAIL
value: "blume.erich@gmail.com"
- name: PAPERLESS_APPS
value: "allauth.socialaccount.providers.openid_connect"
- name: PAPERLESS_SOCIALACCOUNT_PROVIDERS
valueFrom:
secretKeyRef:
name: paperless-secrets
key: socialaccount-providers
- name: PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS
value: "true"
- name: PAPERLESS_SOCIAL_AUTO_SIGNUP
value: "true"
- name: PAPERLESS_ACCOUNT_ALLOW_SIGNUPS
value: "false"
- name: PAPERLESS_REDIRECT_LOGIN_TO_SSO
value: "false"
volumeMounts: &paperless-mounts
- name: data
mountPath: /usr/src/paperless/data
- name: media
mountPath: /usr/src/paperless/media
- name: consume
mountPath: /usr/src/paperless/consume
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
- name: worker
image: registry.ops.eblu.me/blumeops/paperless:kustomized
command: ["celery", "--app", "paperless", "worker", "--loglevel", "INFO"]
env: *paperless-env
volumeMounts: *paperless-mounts
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"
- name: beat
image: registry.ops.eblu.me/blumeops/paperless:kustomized
command: ["celery", "--app", "paperless", "beat", "--loglevel", "INFO"]
env: *paperless-env
volumeMounts: *paperless-mounts
resources:
requests:
memory: "64Mi"
cpu: "20m"
limits:
memory: "256Mi"
- name: consumer
image: registry.ops.eblu.me/blumeops/paperless:kustomized
command: ["paperless-ngx", "document_consumer"]
env: *paperless-env
volumeMounts: *paperless-mounts
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "512Mi"
- name: redis
image: docker.io/library/redis:kustomized
ports:
- containerPort: 6379
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "128Mi"
volumes:
- name: data
emptyDir: {}
- name: media
persistentVolumeClaim:
claimName: paperless-media
- name: consume
emptyDir: {}

View file

@ -0,0 +1,31 @@
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: paperless-secrets
namespace: paperless
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: paperless-secrets
creationPolicy: Owner
data:
- secretKey: db-password
remoteRef:
key: "Paperless (blumeops)"
property: postgresql-password
- secretKey: secret-key
remoteRef:
key: "Paperless (blumeops)"
property: secret-key
- secretKey: admin-password
remoteRef:
key: "Paperless (blumeops)"
property: admin-password
- secretKey: socialaccount-providers
remoteRef:
key: "Paperless (blumeops)"
property: socialaccount-providers

View file

@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paperless-tailscale
namespace: paperless
annotations:
tailscale.com/proxy-class: "default"
tailscale.com/proxy-group: "ingress"
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Paperless"
gethomepage.dev/group: "Home"
gethomepage.dev/icon: "paperless-ngx.png"
gethomepage.dev/description: "Document management"
gethomepage.dev/href: "https://paperless.ops.eblu.me"
gethomepage.dev/pod-selector: "app=paperless"
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: paperless
port:
number: 8000
tls:
- hosts:
- paperless

View file

@ -0,0 +1,19 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: paperless
resources:
- deployment.yaml
- service.yaml
- pv-nfs.yaml
- pvc.yaml
- ingress-tailscale.yaml
- external-secret.yaml
images:
- name: registry.ops.eblu.me/blumeops/paperless
newTag: v2.20.15-1d4cbbf-nix
- name: docker.io/library/redis
newName: registry.ops.eblu.me/blumeops/valkey
newTag: v8.1.7-ecded30

View file

@ -0,0 +1,22 @@
# NFS PersistentVolume for the Paperless document library, mounted from
# ringtail. Same sifaka export (/volume1/paperless) as the minikube PV,
# but a distinct PV name so both clusters can declare it during the
# parallel-run before cutover.
#
# Prerequisite: sifaka must have an NFS rule granting ringtail Read/Write
# (Squash=No mapping) on the paperless share — the same step done for
# immich. See [[sifaka-nfs-from-ringtail]].
apiVersion: v1
kind: PersistentVolume
metadata:
name: paperless-media-nfs-pv-ringtail
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
nfs:
server: sifaka
path: /volume1/paperless

View file

@ -0,0 +1,15 @@
# PersistentVolumeClaim for the Paperless document library on ringtail.
# Binds the NFS PV for sifaka:/volume1/paperless.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-media
namespace: paperless
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: paperless-media-nfs-pv-ringtail
resources:
requests:
storage: 500Gi

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: paperless
namespace: paperless
spec:
selector:
app: paperless
ports:
- name: http
port: 8000
targetPort: 8000
protocol: TCP

View file

@ -0,0 +1,72 @@
# TeslaMate on ringtail k3s — Nix image.
#
# The Nix image's Entrypoint waits for postgres, runs migrations
# (TeslaMate.Release.migrate), then starts the release — so no command
# override is needed. Stateless; all data lives in the teslamate database
# on the ringtail blumeops-pg (DATABASE_HOST already an in-cluster name,
# unchanged from minikube). See [[migrate-wave1-ringtail]].
apiVersion: apps/v1
kind: Deployment
metadata:
name: teslamate
namespace: teslamate
spec:
replicas: 1
selector:
matchLabels:
app: teslamate
template:
metadata:
labels:
app: teslamate
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: teslamate
image: registry.ops.eblu.me/blumeops/teslamate:kustomized
ports:
- containerPort: 4000
env:
- name: DATABASE_USER
value: "teslamate"
- name: DATABASE_PASS
valueFrom:
secretKeyRef:
name: teslamate-db
key: password
- name: DATABASE_NAME
value: "teslamate"
- name: DATABASE_HOST
value: "blumeops-pg-rw.databases.svc.cluster.local"
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: teslamate-encryption
key: key
- name: DISABLE_MQTT
value: "true"
- name: CHECK_ORIGIN
value: "false"
- name: TZ
value: "America/Los_Angeles"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 4000
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 4000
initialDelaySeconds: 10
periodSeconds: 10

View file

@ -0,0 +1,25 @@
# ExternalSecret for TeslaMate database password
#
# Replaces the manual op inject workflow from secret-db.yaml.tpl
#
# 1Password item: "TeslaMate" in blumeops vault
# Field: "db_password"
#
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: teslamate-db
namespace: teslamate
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: teslamate-db
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: TeslaMate
property: db_password

View file

@ -0,0 +1,27 @@
# ExternalSecret for TeslaMate encryption key
#
# Replaces the manual op inject workflow from secret-encryption-key.yaml.tpl
#
# 1Password item: "TeslaMate" in blumeops vault
# Field: "api_enc_key"
#
# This key encrypts Tesla API tokens at rest in the database.
#
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: teslamate-encryption
namespace: teslamate
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: onepassword-blumeops
target:
name: teslamate-encryption
creationPolicy: Owner
data:
- secretKey: key
remoteRef:
key: TeslaMate
property: api_enc_key

View file

@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: teslamate-tailscale
namespace: teslamate
annotations:
tailscale.com/proxy-class: "default"
tailscale.com/proxy-group: "ingress"
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "TeslaMate"
gethomepage.dev/group: "Services"
gethomepage.dev/icon: "teslamate.png"
gethomepage.dev/description: "Tesla data logger"
gethomepage.dev/href: "https://tesla.ops.eblu.me"
gethomepage.dev/pod-selector: "app=teslamate"
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: teslamate
port:
number: 4000
tls:
- hosts:
- tesla

View file

@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: teslamate
resources:
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
- external-secret-db.yaml
- external-secret-encryption-key.yaml
images:
- name: registry.ops.eblu.me/blumeops/teslamate
newTag: v3.0.0-1d4cbbf-nix

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: teslamate
namespace: teslamate
spec:
selector:
app: teslamate
ports:
- port: 4000
targetPort: 4000
type: ClusterIP