feat(k8s/vikunja): deploy Vikunja with Authelia OIDC and restic backups

This commit is contained in:
2026-09-07 14:48:05 +03:00
parent 72fd37d82d
commit 29602e8858
15 changed files with 570 additions and 13 deletions
+144
View File
@@ -0,0 +1,144 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: vikunja-db-backup
namespace: vikunja
labels:
app: vikunja-backup
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
metadata:
labels:
app: vikunja-backup
spec:
restartPolicy: OnFailure
initContainers:
- name: pg-dump
image: postgres:18
imagePullPolicy: IfNotPresent
env:
- name: PGHOST
value: vikunja-db
- name: PGUSER
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_USERNAME
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_PASSWORD
- name: PGDATABASE
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_DATABASE_NAME
command:
- sh
- -c
- pg_dump --clean --if-exists > /backup/dump.sql
volumeMounts:
- name: backup-tmp
mountPath: /backup
containers:
- name: resticprofile
image: creativeprojects/resticprofile:0.32.0
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
resticprofile -c /secrets/profiles.yaml -n vikunja-db backup
resticprofile -c /secrets/profiles.yaml -n vikunja-db copy
env:
- name: B2_ACCOUNT_ID
valueFrom:
secretKeyRef:
name: vikunja-backup-config
key: B2_ACCOUNT_ID
- name: B2_ACCOUNT_KEY
valueFrom:
secretKeyRef:
name: vikunja-backup-config
key: B2_ACCOUNT_KEY
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true
- name: backup-tmp
mountPath: /backup
volumes:
- name: secrets
secret:
secretName: vikunja-backup-config
- name: backup-tmp
emptyDir: {}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: vikunja-files-backup
namespace: vikunja
labels:
app: vikunja-backup
spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
metadata:
labels:
app: vikunja-backup
spec:
restartPolicy: OnFailure
containers:
- name: resticprofile
image: creativeprojects/resticprofile:0.32.0
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
resticprofile -c /secrets/profiles.yaml -n vikunja-files backup
resticprofile -c /secrets/profiles.yaml -n vikunja-files copy
env:
- name: B2_ACCOUNT_ID
valueFrom:
secretKeyRef:
name: vikunja-backup-config
key: B2_ACCOUNT_ID
- name: B2_ACCOUNT_KEY
valueFrom:
secretKeyRef:
name: vikunja-backup-config
key: B2_ACCOUNT_KEY
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true
- name: files
mountPath: /files
readOnly: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
volumes:
- name: secrets
secret:
secretName: vikunja-backup-config
- name: files
persistentVolumeClaim:
claimName: vikunja-files
+114
View File
@@ -0,0 +1,114 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vikunja
namespace: vikunja
labels:
app: vikunja
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: vikunja
template:
metadata:
labels:
app: vikunja
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
# No fsGroup — the NFS subdir provisioner creates directories 0777, and an
# fsGroup chown over NFS is slow and can fail against Synology's root squash.
seccompProfile:
type: RuntimeDefault
containers:
- name: vikunja
image: vikunja/vikunja:2.6.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3456
name: http
protocol: TCP
env:
- name: VIKUNJA_SERVICE_PUBLICURL
value: https://${VIKUNJA_HOST}/
- name: VIKUNJA_SERVICE_TIMEZONE
value: Europe/Kyiv
- name: VIKUNJA_SERVICE_ENABLEREGISTRATION
value: "false"
- name: VIKUNJA_SERVICE_SECRET
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: SERVICE_SECRET
- name: VIKUNJA_DATABASE_TYPE
value: postgres
- name: VIKUNJA_DATABASE_HOST
value: vikunja-db
- name: VIKUNJA_DATABASE_SSLMODE
value: disable
- name: VIKUNJA_DATABASE_USER
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_USERNAME
- name: VIKUNJA_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_PASSWORD
- name: VIKUNJA_DATABASE_DATABASE
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_DATABASE_NAME
- name: VIKUNJA_FILES_BASEPATH
value: /app/vikunja/files
# Authentication is delegated to Authelia over OIDC — local accounts are off,
# so the login page redirects straight to Authelia (single provider).
- name: VIKUNJA_AUTH_LOCAL_ENABLED
value: "false"
- name: VIKUNJA_AUTH_OPENID_ENABLED
value: "true"
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_AUTHELIA_NAME
value: Authelia
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_AUTHELIA_AUTHURL
value: https://auth.${AUTHELIA_DOMAIN}
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_AUTHELIA_CLIENTID
value: vikunja
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_AUTHELIA_SCOPE
value: openid profile email
- name: VIKUNJA_AUTH_OPENID_PROVIDERS_AUTHELIA_CLIENTSECRET
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: OIDC_CLIENT_SECRET
volumeMounts:
- name: files
mountPath: /app/vikunja/files
livenessProbe:
httpGet:
port: 3456
path: /health
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
port: 3456
path: /health
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
volumes:
- name: files
persistentVolumeClaim:
claimName: vikunja-files
+25
View File
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vikunja
namespace: vikunja
annotations:
cert-manager.io/cluster-issuer: letsencrypt
# No Authelia forward-auth middleware — Vikunja authenticates against Authelia
# natively over OIDC. A middleware here would break the OIDC callback and the API.
spec:
tls:
- hosts:
- ${VIKUNJA_HOST}
secretName: vikunja-tls
rules:
- host: ${VIKUNJA_HOST}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vikunja
port:
number: 3456
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: vikunja
+95
View File
@@ -0,0 +1,95 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: vikunja
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-controller
namespace: vikunja
spec:
podSelector:
matchLabels:
app: vikunja
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: vikunja
spec:
podSelector:
matchLabels:
app: vikunja-db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: vikunja
ports:
- port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backup-to-db
namespace: vikunja
spec:
podSelector:
matchLabels:
app: vikunja-db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: vikunja-backup
ports:
- port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backup-egress
namespace: vikunja
spec:
podSelector:
matchLabels:
app: vikunja-backup
policyTypes:
- Egress
egress:
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
- ports:
- port: 8888
protocol: TCP
- ports:
- port: 443
protocol: TCP
- ports:
- port: 5432
protocol: TCP
to:
- podSelector:
matchLabels:
app: vikunja-db
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vikunja-files
namespace: vikunja
spec:
accessModes:
- ReadWriteOnce
storageClassName: nfs-synology-ssd
resources:
requests:
storage: 10Gi
@@ -0,0 +1,26 @@
apiVersion: v1
kind: Secret
metadata:
name: vikunja-backup-config
namespace: vikunja
stringData:
profiles.yaml: ENC[AES256_GCM,data:TS6WByBhixGH/ic/f3veHUd9y0FHMg3auihfL6RXl9APxL4MugcKYTtwflAReuRLaREKjqX1Gf5765moGxfJZUBcGpD+s19rqMXHS++UccMB4q5yr4l0Sx6zfd8IznsjSwLYH2NZzkViurnkELO9YWmgeAddRnbHSgWCxg2EYj32c0du9GKjXBdgNm8R/3nfWY6DpLt1zBW+NoJvlNaTCOWipjDpI1u99WVDiov7+ucvVCvSFRhBLOvUhd8F3++aCyNeaYyXts8YNrxBzneKVixkMvOHah9jB9jHY4nKoIlrYK3Uw5nNAmLQfknQtpuOuQthlmJ/O21R4Yg5JHsWwG4QpFM5o7wsFFNC3qLjyDY2hEFuJi+XxZOTDMHdWbR6jiZ3PIpqE6jNTfiWpNkhysJvXviIQpAk0YnrN4jZLDECek2cmA7UZdwxIVnxxLTtBEYDQlo0SDtEWFd8RsO3FGgBmQWzA9I2fMzc4kj6S6C5bvLim5XNw3keDWWYLFd+Y/bs4MCiQ44fb2x6CsN6J7Y0acX13K1/tVOgujvI4YKfTkYtzH3AeGrj9p/WeV/UiNSABybQXLFXZWjaF3zCb4jRhrG0Bi3wFW6sBAR4R9j9uRUfxU+Ag5unc3hv/tVzZ2fPPzgHLpt+YClBc2JNftjn2oiPtEqUgoijvBXIo1g7ctB6wsNzo897NABanzCZVGRyqijPmxRGrS6lIIqpIVLNtsQNiIJ+CMsHqnEqPxXpUq5U7ufZXZSLP0sLsa89rBn6LE/bQNcChQZg9DTBfq5HR5+zJ1VmpeUE9+Pypu1XrPyks2dAj8JJmkeQU78t/rlroN2nEsdj94D73gkCSQFZvghLWUx0BwrSnzBPhg/9+uaC85FM3z+5DQVgyocABz1AswzcV4PB0tvp8tPbO3GCPmcChBARCQe2UoKcMk2R8ONfzRDgJ9eN1ebd+WJUI6qtR2WxQ+hs8fBFX+e3jo3krRXoIsqQfmH/SgI/Z2K/JCNktd6RsopvPfjX17wfgMvgGxc95TRxcpOc0L2G05prrGubIGCr5O7XsFMTU8gQxG7/I90Y7GwCQPTm0+og59CzdcKfRoDRpDeru6uupvHDBrkuoHMCOEuvcNnTRmqRygmJBTRShDI8esbvmea3kwiq5qzTjUuUSba004lFGr3O4qutqYTJ6odlfRn/AsbvLWpa1q3SWA242ZstO2uWdHc2uoORiaquImMXPmmb4a6dsR13UIrnUvitTRjEdA==,iv:ERenJ0CVR/13qdIdgQvV7TS5DwHFTdx4dXiPX+iFloE=,tag:AJ+mfkPbHZV3OJ6sd/66mw==,type:str]
restic-password-nas: ENC[AES256_GCM,data:nCeCBNHSn3R4925eUtrZPSSE1fn8sONRa4HtlepwB78=,iv:zJOBFzQyYGvEKkOGWhSj5t4rRSu4/DHNPfVOi2qd/Vo=,tag:wl52o8c91Dsb3KgqSdkZTw==,type:str]
restic-password-b2: ENC[AES256_GCM,data:dyzRN9DEzlzJNpVYw5QpSoLOYCy708P3euR6Lw5zjiQ=,iv:tOQDt40GxhExvP/Wmu1aKt0eM8o40tq6fJwV83QW6jI=,tag:htXqf9/DzAl6BCJFKhu6zg==,type:str]
B2_ACCOUNT_ID: ENC[AES256_GCM,data:9GXvx0R5jOGQmoAUNbxTArJsKuo=,iv:AxlXfeBF12re7S/tZFGccPyfxGgZm+X3Rx2PUXRtH/A=,tag:hIGKbRpSIGM8FdtYD90IzQ==,type:str]
B2_ACCOUNT_KEY: ENC[AES256_GCM,data:/sIM4fCEIgZGkFUlxv9IfO6LZCAUTqSlewFBd7M=,iv:DXnMMcNEUsz683bHkLdnMCH8tRUXEqvuwIBqk6XpsiY=,tag:kfb6u9KCkicfDMNd5YVhVw==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrdG84VTMyZUhaVkpEQnJS
anU0VEs0U0l2WFF6K2hQKzVWQy91dkdOMTFNClI0VjQ3WkhjN2FsTUxwbEVYRjUr
QkxIT0JOWWZaUTl5eWM1TFpBM2NRODQKLS0tIGZRNXVlcU1pU0h0N1p6UWdMRDhV
NHFaS3pkTzlQQllzR0ZzMFU0N2hQV28KvFlkIKTi6p2EnGtq3NDYr2OKiSuLV8pM
FpQ54FBj7Qe6AEn6vWAQegRyMWAnOBFLR3q4mf+I4N7uaNh7FyZwQw==
-----END AGE ENCRYPTED FILE-----
recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc
encrypted_regex: ^(data|stringData|email)$
lastmodified: "2026-09-07T11:46:05Z"
mac: ENC[AES256_GCM,data:U2o0x1YBVHlO/RkimK848dMlLqiqUry6Z7orLfgFP6oEJWHWBuAlw17e758TvEK9pUnoAqYBYLikkKA33tgXJv+CHynp1f+0tVr6jZXq4PSQs17IPfAnMYYjTZBCyEgeGz/RePKyud8m6hbXZ6l3DOmea8C7OnVFnUeYyni1GfU=,iv:TaolOm5Ez6iO/S0Qp5XvAZelzUmiJFPaBLbZPEu8bGg=,tag:wOh4bOorG8zV0LKUtFYjdw==,type:str]
version: 3.13.3
+26
View File
@@ -0,0 +1,26 @@
apiVersion: v1
kind: Secret
metadata:
name: vikunja-credentials
namespace: vikunja
stringData:
DB_USERNAME: ENC[AES256_GCM,data:v5ZLlBBCYw==,iv:FEAQixJxjQESAm44z9AubpzSv6VPKSQxpGkoYIxfKbk=,tag:e97BlPYRhZg8UweFyeaf3w==,type:str]
DB_PASSWORD: ENC[AES256_GCM,data:J90Xco62vI8a1KFPBjJNJV1ybisNX+D4LfPebTiXO6c=,iv:j/XY/1K87lfKRCNUSBF0wUH8jx1qlYIHuVjDjh6ia00=,tag:g7yoGK6W3ACbMo/VqYWSuQ==,type:str]
DB_DATABASE_NAME: ENC[AES256_GCM,data:ySn5r6E0Lw==,iv:lkZUN87lavIFzn/4VMpxzQY813gEmqp5zppoiHWQIE0=,tag:roKDQsDlt8qdQ6LUcNH6oQ==,type:str]
SERVICE_SECRET: ENC[AES256_GCM,data:NMliN4ctgGzSUQZ1lxdlis+7oEuI9Wo7AgR/Arad+Rwo3IYnz1dHcfxfozwz4nwO/8NMxfx91KHHKgpKL732Rg==,iv:8RRsKOjWJBDPIbMaiqjSDt78z4uY7RmDW9M9s7vEGzk=,tag:8gEbiZI1SxSpliNf5IYnKg==,type:str]
OIDC_CLIENT_SECRET: ENC[AES256_GCM,data:qg950MtKcwfCbVBGimxgQBSv11jK0SwFsTQAJEbmeINsIDUGGaaSdNNAcB//ccSBSw5SVgJQ/JTq67ZVz/6KjQ==,iv:HriNEfVMiGTA/y6ItBORtQ1hs/G7bYteqtqtx3HWYR8=,tag:kOoLNgD+ZC+nlbOhIqvzsg==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWTGtJSmo4eVBOU3pvdGZT
U1JwYjhaUm41NzBERzF6ek1TWStGSEZHK2pnCjU1RkdWVEVaeXhXc2tKVDlrRmgv
Kzd1N2dXTHRwdTlBNmdpMjdiTFRLSGsKLS0tIGp3cFh4a1YrRVBYamRNbGZxdXNP
c1ZMclQvcHpJR2N0MHNyVDc5VU13NjAKHLYo/UVPtAUydGDjk0cVyrN+m2rMg8AR
alZw1JdIch/Fgb9MPHmDDJwRU1aX8FEedz+pWE9p/UJjh0xIpgy2HQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc
encrypted_regex: ^(data|stringData|email)$
lastmodified: "2026-09-07T11:46:05Z"
mac: ENC[AES256_GCM,data:VSSzkGzEAfo3G6GcG28aQaxG/ZFB0SYILd9JRh8MRCGShWxFaQK/6lSV341xhFFQpOHr87lGC4pIFUVMBZ81Iu2C+QAVSQN9HFEN7R6o2cizNrIycaGaoHB7YenNpJ63EBeCd4E32hXXEXOs6nvRwVlAkaNitMhDXp+myjHU/Tc=,iv:cG2JXmcHsY7HA56FTHzrJj+5XpZW75ynSlNXtV4zHjg=,tag:XYqsGpBBFiInSk5CXEubuQ==,type:str]
version: 3.13.3
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: vikunja-db
namespace: vikunja
spec:
clusterIP: None
selector:
app: vikunja-db
ports:
- port: 5432
targetPort: 5432
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: vikunja
namespace: vikunja
spec:
selector:
app: vikunja
ports:
- port: 3456
targetPort: 3456
name: http
@@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: vikunja-db
namespace: vikunja
labels:
app: vikunja-db
spec:
serviceName: vikunja-db
replicas: 1
selector:
matchLabels:
app: vikunja-db
template:
metadata:
labels:
app: vikunja-db
spec:
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
containers:
- name: postgres
image: postgres:18
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_USERNAME
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_PASSWORD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: vikunja-credentials
key: DB_DATABASE_NAME
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
startupProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 30
livenessProbe:
tcpSocket:
port: 5432
periodSeconds: 30
failureThreshold: 5
readinessProbe:
tcpSocket:
port: 5432
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
memory: 1Gi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: nfs-synology-ssd
resources:
requests:
storage: 5Gi