feat(vikunja): deploy Vikunja 2.6.0 with Pocket ID OIDC (DEV-560)

Namespace-scoped Postgres 16 + unified Vikunja container behind Traefik
ingress at vikunja.basicstack.de. Both pods pinned to Hetzner fsn1 due to
their encrypted PVCs (10 Gi DB, 10 Gi attachments). Local auth disabled;
sign-in only via Pocket ID (client `Vikunja`, groups `vikunja_admins` /
`vikunja_members`). SMTP mailer left disabled pending mailbox provisioning.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO 2026-09-05 09:38:25 +00:00
parent 28a28bf7c0
commit 1743815703
12 changed files with 402 additions and 0 deletions

20
apps/app-vikunja.yaml Normal file
View file

@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vikunja
namespace: argocd
spec:
project: default
source:
repoURL: git@forgejo.forgejo.svc.cluster.local:basicstack/stack.basicstack.de.git
targetRevision: main
path: apps/vikunja
destination:
server: https://kubernetes.default.svc
namespace: vikunja
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

36
apps/vikunja/README.md Normal file
View file

@ -0,0 +1,36 @@
# Vikunja (vikunja.basicstack.de)
Self-hosted task manager. Unified `vikunja/vikunja` container (API + frontend on port 3456). Backed by a namespace-local PostgreSQL. OIDC via Pocket ID (`auth.basicstack.de`); local login disabled.
## Layout
| File | Purpose |
|---|---|
| `vikunja-db-pvc.yaml` | 10 Gi encrypted PVC for Postgres data |
| `vikunja-files-pvc.yaml` | 10 Gi encrypted PVC for user-uploaded attachments (`/app/vikunja/files`) |
| `vikunja-postgres-deployment.yaml` | Postgres 16, pinned to `csi.hetzner.cloud/location=fsn1` |
| `vikunja-postgres-service.yaml` | ClusterIP `vikunja-postgres:5432` |
| `vikunja-deployment.yaml` | Vikunja app, pinned to `fsn1`, waits on Postgres |
| `vikunja-service.yaml` | ClusterIP `vikunja:80` → pod `3456` |
| `vikunja-ingress.yaml` | Traefik + `letsencrypt-prod` cert |
| `vikunja-db-credentials-sealed.yaml` | Postgres user password |
| `vikunja-oidc-secret-sealed.yaml` | Pocket ID client id + secret |
| `vikunja-app-secrets-sealed.yaml` | Vikunja JWT secret |
## Node placement
Both the app and Postgres pin to Hetzner location `fsn1` because their PVCs live on `hcloud-volumes-encrypted` (datacenter-local).
## OIDC
- Pocket ID client `Vikunja` (id `cdfff4ee-28ba-4aab-9f47-7e41b4b6b847`), callback `https://vikunja.basicstack.de/auth/openid/basicstack`.
- Pocket ID groups `vikunja_admins` and `vikunja_members`.
- Vikunja does not map OIDC groups to the admin flag. Promote the first user post-login: `kubectl -n vikunja exec deploy/vikunja -- /app/vikunja/vikunja user chmod --admin <username>`.
## Optional: SMTP
`VIKUNJA_MAILER_ENABLED=false` today. To enable reminder emails, create a Stalwart mailbox `vikunja@basicstack.de`, add its credentials as a sealed secret `vikunja-mailer`, and set `VIKUNJA_MAILER_ENABLED=true` plus host/port/from env vars.
## Change control
Manifest changes flow through Argo CD (Application: `apps/app-vikunja.yaml`, auto-sync + prune + selfHeal). Sealed secrets are encrypted against the cluster-active sealed-secrets key.

View file

@ -0,0 +1,15 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: vikunja-app-secrets
namespace: vikunja
spec:
encryptedData:
jwt-secret: AgA2gSMP5tLy+2BeEe/vO3DHi7v4WO2HRZc2kTD/+BRII/CxCd3SIJYOh5O7OPNOq5Vp+F6oCi6GUg7ljyXuHU1IlHSSYWfjdGKS/mIvlKf2XRASOS1CefNSDKjU+x2FB3LPJG10v/0OG7bLKCj7QGq5y+QNc8dQrV7kqwAonG18iZzl5rcL4vN38B2V6kaO+s/tcz0uwmVOlUqblmv6WwrkMm5bVt9RQxcO5UjmDuf2KlnHBgvhRUhcBCbmrnI7J+FkVtq+2hiWwyz3hXAQAyy6ULMltYZvSy4ADseTV3yBdArUvY9e13kwfVZBjLe5EBzn0SKoQyY0XZQUYDmfQU+U5zFxsZLbYeekf7FUN8zHJ+mgvrT35PGrVazoz2rKI+9BrA7sstYJZ0ZUlj+nKnrbHYyaUhrlu3DYFvTHCOz8ylRxIsM5Ux819+RISpBhKkg6+mIhH2L26larI7UEY576c83TMhmCEOeX0es+MoomsAqeQi8vZ+OT5qz9dFZpzshXmwRYy1Ezm9d992iBF9EskB9IuyaozpA+GU9WwKAOj5GF3izj89nel9QPPQVBGHYvxXXfaXi3yQIpqD54nVPKC1oEhPNeCfVp3YcGSd/HOsl3tT/8G7ipD40jGVk5tgWxpymbvNZmcpm1CIb4SPYHOavJtqBcPY1F9k8a5yYl9sMqd64qzDmTlOQhRVWjD0mqCf2W/W3OvHo4dTu6Fy95jHrzoHsmmfNQQDVytCJK/3SmYUvh51nD8/CyiJxRAq3zPViFHKmRS0E5oCR0+zm3GlZTGwUmu1ROSIlsDgik/khmrZvYFLtF
template:
metadata:
creationTimestamp: null
name: vikunja-app-secrets
namespace: vikunja

View file

@ -0,0 +1,15 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: vikunja-db-credentials
namespace: vikunja
spec:
encryptedData:
postgres-password: AgB+P7iBg+tPIDKMGmiPh9HREVb7mKCf1GvQfBLcF61QuWHt0/S1YBYCd4fF3PSA9ok916m6VeOoIQKB0p57XDSEeNewYNsXQgudS0k0JHNdRZER8JhbXpK53E7EY0xw4TD8Tc6WFqyzCFwsL1WpYvvDH3p25W8f3JJfB39D6Up9j5xT5Hufi0G5BJ+tbAujNJooa0SWZbtQPxMWG96fvjmTV5FKxzvwg1JOL4gq0JHBfzo9sKIbQdGpGJjX4JkPLCxA0jU6mcJKEFn80zkDbQBL3g5dXZaYmIAEogZkvytltR4ebI0zyDrSTPqXhJF5bFcRpM43VXz6ZcjgdtG5VPelvAuMqfJ1U7HdYhhJqTt0lvwUi03BjulIJJ52gUjlg2yQioOwfON/y3gRM1TgMHMFCyw32S7vCfR00MKVh7xrb4YceRUTbeHkTd+4TMcdpI11YHeah2scu82tzyD/cGPtpFv2LYyH7JrBcIXXhfIDccBRX4bb2se9VyhpmvEP5Q9SQjazo/BxdnJUYF6kzpJA9oOIjFMXcSDxZh/nwDkA90w4a9iyT9l0+7p3z/RjbXOMKybuQlXpfPrhtCRS6PM3Am5sgnIi/APRaujDa/2a3kCe7K0zZl4Wk3wlAc4mMjnoWs9w//HfMpQByM6GWeHWeoQ/Bj3xCVtACb6SNqTxIs4OP9hAVBnHqouH0O/zjqZNuBa5v7GDwmW6JU1cA+Y/UI4Z/ycu2X6AXBHp5CROheV9PdINTrgS
template:
metadata:
creationTimestamp: null
name: vikunja-db-credentials
namespace: vikunja

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vikunja-db
namespace: vikunja
labels:
app: vikunja
component: database
spec:
accessModes:
- ReadWriteOnce
storageClassName: hcloud-volumes-encrypted
resources:
requests:
storage: 10Gi

View file

@ -0,0 +1,131 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vikunja
namespace: vikunja
labels:
app: vikunja
component: app
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: vikunja
component: app
template:
metadata:
labels:
app: vikunja
component: app
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: csi.hetzner.cloud/location
operator: In
values:
- fsn1
initContainers:
- name: wait-for-postgres
image: postgres:16-alpine
command:
- sh
- -c
- |
until pg_isready -h vikunja-postgres -U vikunja; do
echo "Waiting for PostgreSQL..."
sleep 2
done
securityContext:
fsGroup: 1000
containers:
- name: vikunja
image: vikunja/vikunja:2.6.0
ports:
- name: http
containerPort: 3456
env:
- name: VIKUNJA_SERVICE_PUBLICURL
value: https://vikunja.basicstack.de/
- name: VIKUNJA_SERVICE_JWTSECRET
valueFrom:
secretKeyRef:
name: vikunja-app-secrets
key: jwt-secret
- name: VIKUNJA_SERVICE_ENABLEREGISTRATION
value: "false"
- name: VIKUNJA_SERVICE_TIMEZONE
value: Europe/Berlin
- name: VIKUNJA_DATABASE_TYPE
value: postgres
- name: VIKUNJA_DATABASE_HOST
value: vikunja-postgres
- name: VIKUNJA_DATABASE_PORT
value: "5432"
- name: VIKUNJA_DATABASE_DATABASE
value: vikunja
- name: VIKUNJA_DATABASE_USER
value: vikunja
- name: VIKUNJA_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: vikunja-db-credentials
key: postgres-password
- name: VIKUNJA_DATABASE_SSLMODE
value: disable
- name: VIKUNJA_FILES_BASEPATH
value: /app/vikunja/files
- name: VIKUNJA_AUTH_LOCAL_ENABLED
value: "false"
- name: VIKUNJA_AUTH_OPENID_ENABLED
value: "true"
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_BASICSTACK_NAME
value: BasicStack SSO
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_BASICSTACK_AUTHURL
value: https://auth.basicstack.de
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_BASICSTACK_SCOPE
value: openid profile email
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_BASICSTACK_CLIENTID
valueFrom:
secretKeyRef:
name: vikunja-oidc-secret
key: client-id
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_BASICSTACK_CLIENTSECRET
valueFrom:
secretKeyRef:
name: vikunja-oidc-secret
key: client-secret
- name: VIKUNJA_MAILER_ENABLED
value: "false"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /api/v1/info
port: 3456
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /api/v1/info
port: 3456
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
volumeMounts:
- name: files
mountPath: /app/vikunja/files
volumes:
- name: files
persistentVolumeClaim:
claimName: vikunja-files

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vikunja-files
namespace: vikunja
labels:
app: vikunja
component: files
spec:
accessModes:
- ReadWriteOnce
storageClassName: hcloud-volumes-encrypted
resources:
requests:
storage: 10Gi

View file

@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vikunja
namespace: vikunja
labels:
app: vikunja
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- vikunja.basicstack.de
secretName: vikunja-tls
rules:
- host: vikunja.basicstack.de
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vikunja
port:
number: 80

View file

@ -0,0 +1,16 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: vikunja-oidc-secret
namespace: vikunja
spec:
encryptedData:
client-id: AgBdzdW0SxhmOyhOpC/jkBJ4UtD0qrRrytVp7fInK3bxiiVBHsIU8r3Hoe2iYZU75IyITogmcGImKH9pivxPMhj5jBqFeCExQadt/pqXHL9TmxmNpIx7FmnlrxaZ+zeqeKRMDhYJnxSeNiuRDVZEEnsBhmUc0gvz8Pq/x82ExY++MQjQQu2xB6XQ7DTyzVDx32yZAvBU6aRFhTtMTqPAvjsMHp6pKmciauS7LmffCVIJvnJUaUKIELHPDyWsMcQvWpPQrPKGkvmGruhXbct6LxPFKdkbsQMUWvj7emSemmbJk0VmGPtVxI7cTmHIJ6l4kr8aJfJ9fv2gMzbgprYVZHJ17qhPvvUv5iTUcYu5FiMiy3QtNHYMP+VyhWvj9aUH8ypFYwf0S3/fRobEtyBxnBLCmnIt4lvS0ABQ49YwdtWnBUXT1jP4Xzk3gWM8fe1LsOuUZXMgFoGMXtxuodhZFhTELjdKPaYTY/DDU6iRMpk/nUsQy1IGls1CySEB+taiQDRNMZmQAFHdoFOxX8sZyPKdYL3wgKDweqcLoI0UKjapkHdpcoWccqg0ky/f5VWWlD7kHWLXYeosH6ucLAKR/mRHoyOfIy1dEpYYevfv5UM4Lewnr8epLse/x3ymsBNaSIhxt8PxnFYe/Gi1rO8MAgTPNcw2pr2z5jiW8CXUPLdXezleoctl4VGg97pkYVibn0BJ+VrZOTgO/p/df5D0v7h7DSsvdbpA7XxkaTnL62JRhC3RDXU=
client-secret: AgA3ZQ5Da1D2tcNBA9RVf7YYNLBvBjKf+OArLLQSKKUDGAULoC0BQUn+T7rctuOANFeKLOYE2OjJQhURoi69xFyQ3d868s4a2Ipo3QwUV2vtoVJnz7l2fGXCzAOTcJhKonHdJooklfeK+gwi80sRXV1IxxDu778yKSWnFUd61vUPbhKgVgtXDM4IItNtctGc4Drz6dvrOHkyM1m4kxGbXUclpN+HztufQrO7W+ba23bZRCejaYz3sQ6IHmM50HXzRPD5Rdb2ALQFm9V7zdSuUQ8njNrXHfgqMSiDWoCzdpTKG50mBPqmait/yyNVnOAd8J906RZk27LnAY9zV1WiMUSImNS6IzuLV0DZTn6I4cWgnemIwNAdNR4/x7TwWNdVS3A3jH8q6S1IP5g65P40SmP27PfFx+L7jx72Om8HXBY8TpMtWwGFcjyEGy61tKuimXd65+o0UkXEmQfJKfnugnSM3yCufEat2PIE2+Arb7+pRh6VtzaBowLdo32OcqS5GMmFLhisv5cR016IIIbSnaLkIGpiZEHSvpcLoGOJm8H+1P6MPsHWfjWtKZy6y8Ri/B9zNHlUx506Kij5YOR+tH3B3ip4PIa2LRlRCAbzBb/WCwzP72WzK8PU/sSujTa+JgxBoNGt3fg2iqBmoaU3UrhVoTTcXhEn7pnvIlk60jZKTscC+thS++VHMnf7LjLAA2ksu9G6qKpWy5DAERYi/MC6YdYz0eoIBSa/NEHLi94zFA==
template:
metadata:
creationTimestamp: null
name: vikunja-oidc-secret
namespace: vikunja

View file

@ -0,0 +1,79 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vikunja-postgres
namespace: vikunja
labels:
app: vikunja
component: database
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: vikunja
component: database
template:
metadata:
labels:
app: vikunja
component: database
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: csi.hetzner.cloud/location
operator: In
values:
- fsn1
containers:
- name: postgres
image: postgres:16-alpine
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_DB
value: vikunja
- name: POSTGRES_USER
value: vikunja
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: vikunja-db-credentials
key: postgres-password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
livenessProbe:
exec:
command:
- pg_isready
- -U
- vikunja
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- vikunja
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: vikunja-db

View file

@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: vikunja-postgres
namespace: vikunja
labels:
app: vikunja
component: database
spec:
type: ClusterIP
selector:
app: vikunja
component: database
ports:
- name: postgres
port: 5432
targetPort: 5432

View file

@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: vikunja
namespace: vikunja
labels:
app: vikunja
component: app
spec:
type: ClusterIP
selector:
app: vikunja
component: app
ports:
- name: http
port: 80
targetPort: 3456