first commit

This commit is contained in:
sttlab
2026-07-22 14:41:10 +00:00
commit e1577fe061
35 changed files with 1847 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# nginx config for the digest file server: directory listing (autoindex)
# over the generated Markdown digests, no app logic involved.
apiVersion: v1
kind: ConfigMap
metadata:
name: digest-files-nginx-conf
namespace: watch
data:
default.conf: |
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
location / {
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
}
}
+61
View File
@@ -0,0 +1,61 @@
# Read-only HTTP file server exposing the digest workflow's output
# (n8n-digest-workspace PVC, digests/ subdirectory) for browsing/download,
# instead of email delivery (no SMTP configured).
apiVersion: apps/v1
kind: Deployment
metadata:
name: digest-files
namespace: watch
labels:
app.kubernetes.io/name: digest-files
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: digest-files
template:
metadata:
labels:
app.kubernetes.io/name: digest-files
spec:
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:alpine
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: digests
mountPath: /usr/share/nginx/html
subPath: digests
readOnly: true
- name: nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
readOnly: true
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 20
volumes:
- name: digests
persistentVolumeClaim:
claimName: n8n-digest-workspace
- name: nginx-conf
configMap:
name: digest-files-nginx-conf
+20
View File
@@ -0,0 +1,20 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: digest-files
namespace: watch
spec:
gatewayClassName: envoy
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: digests.sttlab.pc
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: sttlab-pc-tls
+21
View File
@@ -0,0 +1,21 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: digest-files
namespace: watch
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: digest-files
hostnames:
- digests.sttlab.pc
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- kind: Service
name: digest-files
port: 80
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: digest-files
namespace: watch
labels:
app.kubernetes.io/name: digest-files
spec:
selector:
app.kubernetes.io/name: digest-files
ports:
- name: http
port: 80
targetPort: 8080
+35
View File
@@ -0,0 +1,35 @@
# Runs miniflux-initializer (see /miniflux-initializer) to create the
# categories/feeds from feeds.yaml via the Miniflux REST API. Idempotent,
# safe to re-run: delete the Job then re-apply after editing feeds.yaml.
# kubectl delete job bootstrap-feeds -n watch --ignore-not-found
# kubectl apply -f k8s/miniflux/bootstrap-feeds-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: bootstrap-feeds
namespace: watch
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: bootstrap-feeds
image: docker.io/library/miniflux-initializer:latest
imagePullPolicy: Never
env:
- name: MINIFLUX_URL
# Trailing dot makes this an absolute FQDN: musl's resolver
# (Alpine base image) mishandles ndots-relative lookups here
# and fails with EAI_AGAIN otherwise.
value: http://miniflux.watch.svc.cluster.local.
- name: MINIFLUX_USERNAME
valueFrom:
secretKeyRef:
name: miniflux-credentials
key: ADMIN_USERNAME
- name: MINIFLUX_PASSWORD
valueFrom:
secretKeyRef:
name: miniflux-credentials
key: ADMIN_PASSWORD
+28
View File
@@ -0,0 +1,28 @@
# One-off Job to create the "miniflux" database on the shared Postgres
# instance. Re-run (delete + kubectl apply) whenever a new app needs a
# database, with a copy of this Job using a different DB name.
apiVersion: batch/v1
kind: Job
metadata:
name: create-db-miniflux
namespace: postgres
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: create-db
image: postgres:18-alpine
envFrom:
- secretRef:
name: postgres-admin
command:
- sh
- -c
- |
set -e
psql "postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres-postgresql:5432/postgres" \
-tc "SELECT 1 FROM pg_database WHERE datname = 'miniflux'" | grep -q 1 \
|| psql "postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres-postgresql:5432/postgres" \
-c "CREATE DATABASE miniflux"
+52
View File
@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: miniflux
namespace: watch
labels:
app.kubernetes.io/name: miniflux
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: miniflux
template:
metadata:
labels:
app.kubernetes.io/name: miniflux
spec:
containers:
- name: miniflux
image: miniflux/miniflux:latest
ports:
- name: http
containerPort: 8080
envFrom:
- secretRef:
name: miniflux-credentials
env:
- name: RUN_MIGRATIONS
value: "1"
- name: CREATE_ADMIN
value: "1"
- name: BASE_URL
value: https://miniflux.sttlab.pc
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 768Mi
readinessProbe:
httpGet:
path: /healthcheck
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthcheck
port: http
initialDelaySeconds: 15
periodSeconds: 20
+20
View File
@@ -0,0 +1,20 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: miniflux
namespace: watch
spec:
gatewayClassName: envoy
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: miniflux.sttlab.pc
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: sttlab-pc-tls
+21
View File
@@ -0,0 +1,21 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: miniflux
namespace: watch
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: miniflux
hostnames:
- miniflux.sttlab.pc
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- kind: Service
name: miniflux
port: 80
+15
View File
@@ -0,0 +1,15 @@
# Copy to secret.yaml, fill in real values, and apply with:
# kubectl apply -f secret.yaml
# Never commit the filled-in secret.yaml to git.
#
# POSTGRES_USER/POSTGRES_PASSWORD must match k8s/postgres/secret.yaml.
apiVersion: v1
kind: Secret
metadata:
name: miniflux-credentials
namespace: watch
type: Opaque
stringData:
DATABASE_URL: postgres://postgres:changeme@postgres-postgresql.postgres.svc.cluster.local:5432/miniflux?sslmode=disable
ADMIN_USERNAME: admin
ADMIN_PASSWORD: changeme
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: miniflux
namespace: watch
labels:
app.kubernetes.io/name: miniflux
spec:
selector:
app.kubernetes.io/name: miniflux
ports:
- name: http
port: 80
targetPort: 8080
+27
View File
@@ -0,0 +1,27 @@
# One-off Job to create the "n8n" database on the shared Postgres instance.
# Mirrors k8s/miniflux/create-db-job.yaml.
apiVersion: batch/v1
kind: Job
metadata:
name: create-db-n8n
namespace: postgres
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: create-db
image: postgres:18-alpine
envFrom:
- secretRef:
name: postgres-admin
command:
- sh
- -c
- |
set -e
psql "postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres-postgresql:5432/postgres" \
-tc "SELECT 1 FROM pg_database WHERE datname = 'n8n'" | grep -q 1 \
|| psql "postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres-postgresql:5432/postgres" \
-c "CREATE DATABASE n8n"
+97
View File
@@ -0,0 +1,97 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: n8n
namespace: watch
labels:
app.kubernetes.io/name: n8n
spec:
replicas: 1
strategy:
type: Recreate # single ReadWriteOnce PVC, avoid two pods mounting it at once
selector:
matchLabels:
app.kubernetes.io/name: n8n
template:
metadata:
labels:
app.kubernetes.io/name: n8n
spec:
securityContext:
fsGroup: 1000 # matches the "node" user baked into the n8n image
containers:
- name: n8n
image: n8nio/n8n:latest
ports:
- name: http
containerPort: 5678
envFrom:
- secretRef:
name: n8n-credentials
env:
- name: DB_TYPE
value: postgresdb
- name: DB_POSTGRESDB_HOST
value: postgres-postgresql.postgres.svc.cluster.local
- name: DB_POSTGRESDB_PORT
value: "5432"
- name: DB_POSTGRESDB_DATABASE
value: n8n
- name: N8N_HOST
value: n8n.sttlab.pc
- name: N8N_PORT
value: "5678"
- name: N8N_PROTOCOL
value: https
- name: N8N_WEBHOOK_URL
value: https://n8n.sttlab.pc/
- name: GENERIC_TIMEZONE
value: Europe/Paris
- name: TZ
value: Europe/Paris
- name: N8N_RESTRICT_FILE_ACCESS_TO
value: "/data" # single path only (no list support)
volumeMounts:
- name: data
mountPath: /home/node/.n8n
- name: digest-workspace
mountPath: /data # dedicated volume for Read/Write File nodes, separate from n8n's own
# internal directory (which n8n always blocks file-node access to, regardless of
# N8N_RESTRICT_FILE_ACCESS_TO, via N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES)
- name: digest-prompt
mountPath: /data/prompts/digest.md
subPath: digest.md # avoids the ConfigMap symlink indirection, which trips n8n's file-access allowlist check
readOnly: true
- name: digest-prompt
mountPath: /data/prompts/editorial.md
subPath: editorial.md
readOnly: true
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 20
volumes:
- name: data
persistentVolumeClaim:
claimName: n8n-data
- name: digest-workspace
persistentVolumeClaim:
claimName: n8n-digest-workspace
- name: digest-prompt
configMap:
name: n8n-digest-prompt
+20
View File
@@ -0,0 +1,20 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: n8n
namespace: watch
spec:
gatewayClassName: envoy
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: n8n.sttlab.pc
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: sttlab-pc-tls
+21
View File
@@ -0,0 +1,21 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: n8n
namespace: watch
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: n8n
hostnames:
- n8n.sttlab.pc
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- kind: Service
name: n8n
port: 80
@@ -0,0 +1,20 @@
# Copy to miniflux-token-secret.yaml, fill in the real token, and apply with:
# kubectl apply -f miniflux-token-secret.yaml
# Never commit the filled-in file to git.
#
# Generate the token in the Miniflux UI under Settings -> API Keys (no
# public REST endpoint creates one). Kept here as the source of truth /
# backup; the digest workflow itself should use an n8n "Header Auth"
# credential (header name X-Auth-Token) created in the n8n UI from these
# same values, not a raw env var — n8n blocks $env access from workflow
# expressions by default (N8N_BLOCK_ENV_ACCESS_IN_NODE), so this Secret is
# not wired into the n8n Deployment's envFrom.
apiVersion: v1
kind: Secret
metadata:
name: miniflux-api-token
namespace: watch
type: Opaque
stringData:
MINIFLUX_URL: https://miniflux.sttlab.pc/v1/
MINIFLUX_API_TOKEN: changeme
+26
View File
@@ -0,0 +1,26 @@
# Persists /home/node/.n8n (encryption key backup, local binary data cache,
# community nodes). Workflow/execution data itself lives in Postgres.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: n8n-data
namespace: watch
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 2Gi
---
# Separate volume for Read/Write File nodes (digest prompt + output), kept
# apart from n8n-data (n8n's own internal directory, which n8n always
# blocks file-node access to regardless of N8N_RESTRICT_FILE_ACCESS_TO).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: n8n-digest-workspace
namespace: watch
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
+19
View File
@@ -0,0 +1,19 @@
# Copy to secret.yaml, fill in real values, and apply with:
# kubectl apply -f secret.yaml
# Never commit the filled-in secret.yaml to git.
#
# DB_POSTGRESDB_USER/PASSWORD must match k8s/postgres/secret.yaml.
#
# N8N_ENCRYPTION_KEY encrypts credentials stored in the n8n database.
# Generate once with `openssl rand -hex 24` and never change it afterwards
# (rotating it locks n8n out of every credential already saved).
apiVersion: v1
kind: Secret
metadata:
name: n8n-credentials
namespace: watch
type: Opaque
stringData:
DB_POSTGRESDB_USER: postgres
DB_POSTGRESDB_PASSWORD: changeme
N8N_ENCRYPTION_KEY: changeme
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: n8n
namespace: watch
labels:
app.kubernetes.io/name: n8n
spec:
selector:
app.kubernetes.io/name: n8n
ports:
- name: http
port: 80
targetPort: 5678
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: watch
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: postgres
+16
View File
@@ -0,0 +1,16 @@
# Copy to secret.yaml, fill in a real password, and apply with:
# kubectl apply -f secret.yaml
# Never commit the filled-in secret.yaml to git.
#
# This is the Postgres superuser used to provision one database per
# application (see create-db-*-job.yaml). Applications connect with these
# same credentials against their own database (no per-app roles for now).
apiVersion: v1
kind: Secret
metadata:
name: postgres-admin
namespace: postgres
type: Opaque
stringData:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: changeme
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: postgres
labels:
app.kubernetes.io/name: postgres
spec:
clusterIP: None
selector:
app.kubernetes.io/name: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
+61
View File
@@ -0,0 +1,61 @@
# Shared PostgreSQL instance, reusable across applications (miniflux, n8n, ...).
# Each application gets its own database (see create-db-*-job.yaml), all
# reachable through the "postgres" Service below.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: postgres
labels:
app.kubernetes.io/name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: postgres
template:
metadata:
labels:
app.kubernetes.io/name: postgres
spec:
containers:
- name: postgres
image: postgres:18-alpine
ports:
- name: postgres
containerPort: 5432
envFrom:
- secretRef:
name: postgres-admin
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
exec:
command: ["pg_isready", "-U", "$(POSTGRES_USER)"]
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
exec:
command: ["pg_isready", "-U", "$(POSTGRES_USER)"]
initialDelaySeconds: 15
periodSeconds: 20
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi