feat(k8s/firefly): add Firefly III with PostgreSQL and backups

This commit is contained in:
2026-04-21 23:35:41 +03:00
parent 271a42e80b
commit f65bc8505b
12 changed files with 480 additions and 7 deletions

View File

@@ -27,6 +27,7 @@ homelab-v2/
├── app/
│ ├── archmirror/
│ ├── external/ # External service vars (e.g. Home Assistant)
│ ├── firefly/
│ ├── grocy/
│ ├── homepage/
│ ├── immich/
@@ -44,6 +45,7 @@ homelab-v2/
| Service | Description |
|---------|-------------|
| **Firefly III** | Personal finance manager |
| **Immich** | Photo and video management with face recognition |
| **Jellyfin** | Media streaming with Intel GPU hardware transcoding |
| **Media Stack** | Sonarr, Radarr, Prowlarr, qBittorrent — automated media acquisition |

View File

@@ -0,0 +1,144 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: firefly-db-backup
namespace: firefly
labels:
app: firefly-backup
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
metadata:
labels:
app: firefly-backup
spec:
restartPolicy: OnFailure
initContainers:
- name: pg-dump
image: postgres:17
imagePullPolicy: IfNotPresent
env:
- name: PGHOST
value: firefly-db
- name: PGUSER
valueFrom:
secretKeyRef:
name: firefly-credentials
key: DB_USERNAME
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: firefly-credentials
key: DB_PASSWORD
- name: PGDATABASE
valueFrom:
secretKeyRef:
name: firefly-credentials
key: DB_DATABASE
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 firefly-db backup
resticprofile -c /secrets/profiles.yaml -n firefly-db copy
env:
- name: B2_ACCOUNT_ID
valueFrom:
secretKeyRef:
name: firefly-backup-config
key: B2_ACCOUNT_ID
- name: B2_ACCOUNT_KEY
valueFrom:
secretKeyRef:
name: firefly-backup-config
key: B2_ACCOUNT_KEY
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true
- name: backup-tmp
mountPath: /backup
volumes:
- name: secrets
secret:
secretName: firefly-backup-config
- name: backup-tmp
emptyDir: {}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: firefly-uploads-backup
namespace: firefly
labels:
app: firefly-backup
spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
metadata:
labels:
app: firefly-backup
spec:
restartPolicy: OnFailure
containers:
- name: resticprofile
image: creativeprojects/resticprofile:0.32.0
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
resticprofile -c /secrets/profiles.yaml -n firefly-uploads backup
resticprofile -c /secrets/profiles.yaml -n firefly-uploads copy
env:
- name: B2_ACCOUNT_ID
valueFrom:
secretKeyRef:
name: firefly-backup-config
key: B2_ACCOUNT_ID
- name: B2_ACCOUNT_KEY
valueFrom:
secretKeyRef:
name: firefly-backup-config
key: B2_ACCOUNT_KEY
volumeMounts:
- name: secrets
mountPath: /secrets
readOnly: true
- name: uploads
mountPath: /uploads
readOnly: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
volumes:
- name: secrets
secret:
secretName: firefly-backup-config
- name: uploads
persistentVolumeClaim:
claimName: firefly-firefly-iii

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: firefly

View File

@@ -0,0 +1,95 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: firefly
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-controller
namespace: firefly
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: firefly-iii
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: firefly
spec:
podSelector:
matchLabels:
app: firefly-db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: firefly-iii
ports:
- port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backup-to-db
namespace: firefly
spec:
podSelector:
matchLabels:
app: firefly-db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: firefly-backup
ports:
- port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backup-egress
namespace: firefly
spec:
podSelector:
matchLabels:
app: firefly-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: firefly-db

View File

@@ -0,0 +1,75 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: firefly-iii
namespace: flux-system
spec:
chart:
spec:
chart: firefly-iii
version: 1.9.13
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: firefly-iii
namespace: flux-system
targetNamespace: firefly
interval: 1m0s
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
deploymentStrategyType: Recreate
podSecurityContext:
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
memory: 512Mi
persistence:
enabled: true
storageClassName: nfs-synology-ssd
storage: 5Gi
config:
existingSecret: firefly-credentials
env:
DB_HOST: firefly-db
DB_CONNECTION: pgsql
DB_PORT: "5432"
DB_DATABASE: firefly
DB_USERNAME: firefly
TZ: Europe/Kyiv
TRUSTED_PROXIES: "10.244.0.0/24"
APP_URL: https://${FIREFLY_HOST}
AUTHENTICATION_GUARD: remote_user_guard
AUTHENTICATION_GUARD_HEADER: Remote-Email
AUTHENTICATION_GUARD_EMAIL: Remote-Email
cronjob:
enabled: true
auth:
existingSecret: firefly-credentials
secretKey: STATIC_CRON_TOKEN
schedule: "0 3 * * *"
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt
traefik.ingress.kubernetes.io/router.middlewares: authelia-chain-authelia-authelia-auth@kubernetescrd
hosts:
- ${FIREFLY_HOST}
tls:
- secretName: firefly-tls
hosts:
- ${FIREFLY_HOST}

View File

@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: firefly-iii
namespace: flux-system
spec:
interval: 1m0s
url: https://firefly-iii.github.io/kubernetes

View File

@@ -0,0 +1,26 @@
apiVersion: v1
kind: Secret
metadata:
name: firefly-backup-config
namespace: firefly
stringData:
profiles.yaml: ENC[AES256_GCM,data:2IDoUurD5tw5uTUX/rWuhGZbaCcYcR/J2ClSdr61rq5w1jjZunnyqeY8Vrp2LSaiDhYd6nIHB0oeM8HVsvkzbLksXVzbS/4/098KEJtey3I7yxfvvlgu9ZKbw0wuLqeXjfEkl7cArTdjMKwA3/B5/Jmqw9Q0g5QUPP97MF12Hagg/kpKipXD/n5YoOtv7ZqfHnxrIIh6XXIpa98pJ58x+7ct7K7DeUUj1s/3WAI7idJ4kTexwOb/NVup9qpheuxqAZmF0z8YUC73W0qzQzgUAOJ4+O09xc15PFRq19a95W9SnSHn8Ao56S67s26d2Hbp4JCa8U6MxHDJ09IdHzpu/bmGAVL6LxDheegYOLZWJtNoAryyE6sQN2PJQxIwFngKaMvs6CHSrZWIbCfKzIhNrJPr/u/l5pDvCSMcvg2oX7Bkc2uJ4WY/yFXSHhaWIdJv0VSvcUH04li1inrZe9AYfnsajRVuzgXrnhonqy/2NXrH32leSVjEeU97+Ma1eOX/t3+SNBglL5kJxJIRJ/tSkPDe/9GpVr+2QNQUmRMuIpSsRNQgGODU/XCFrMyJ1Ikl+pKhHitOELfckJ3FuZPRJB6HEqfkE2fZLK5e2G0JRJ4FinYFo6HkXzk4IAzLDlN2E7kgyPyCqHJQt4C/X+OTwE6MErPEPgE5o9pwC8TGv+Q8AN6rRdYs7FPBR9jxqWxoSTCdz49sVf/kcgcWnicn8XmFEGdWTWk3MqKDe8YdN6fyqPha2lxpsz7FkZMcTV2ZrLvIR65lkmL9Vb1I7SwroYuTr+sj3jTAJLZXgRTXpKodc2zTQR9rQEKvA1C7YgchTJa4axDkMY37MDHZnHPW6MEmPlpPw8NMge6QfNYDTAWvrwThGWQ051RSm8KbYfm+qlMWqx+zCFRcypKwBUKVWuf7TYa0jnL9u4QrUvpLBAr6jErNRdRLgm79h80n/ucPEBLCHlzhPzHGW/evdWMDTCd0B5dYbP0zpPkGEmzfBfnH23ru48DotzPV/y0j+SNacgUXQPFkC5x50jOS2Fay+5qInOyA75aSiFD5EMZmf8gx1a3+rDtUt+6Qict8Umtb3ZzpTrWxvxbV5oxCXNwWShSLAFyfwoNE1sEq++xu+Jp0SzTuSg+Kxz+CmuCJ4bnbYkrhJxyKfw==,iv:WdIFJmhmZ8S+ma5l5SS7de2PrxZJgKK2njrPbLVQSuo=,tag:/mPn6QHatxQYwsZInrK30w==,type:str]
restic-password-nas: ENC[AES256_GCM,data:RIn57MOwyqVGR/GuVpIXi+p5jZ2UQm4VgioCgE1GXIk=,iv:DcF6cD+AVa0LoXY6+wACfQvoAtdndl0chmPcm463fEE=,tag:RK9FF1RMFsfHGmg+ZcBEVA==,type:str]
restic-password-b2: ENC[AES256_GCM,data:nIcy6uJY4fCjQH3aVggt3meckNAytAsw/ZNnGBBm7DM=,iv:zPf9YTDgT9dlq1nlyW6weMjZ0b3eVAuB15Lyx897xGU=,tag:+x0Sn3UQZEyhdMYrTK+Nqg==,type:str]
B2_ACCOUNT_ID: ENC[AES256_GCM,data:t4YqtZRIpLpP7QHGBTeZ8vCL2L+1BjbLug==,iv:TvsBorT7r5A1d1AjPbYZ2vnm0x7gCPe4qY2bXFoK66Y=,tag:zaRhrGV/4siiWiykPlF6Gw==,type:str]
B2_ACCOUNT_KEY: ENC[AES256_GCM,data:xZjFBj7oT2FcKHdwGBzyD1xR/bKtHGLEBsnAqblE+g==,iv:gtJfZvTZucbx6983OOk7WdD8KhJtrW6Wv0hnECYXQ8s=,tag:DCvHrL1Wv3NodzbH5aXylQ==,type:str]
sops:
age:
- recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRSVBheDRtOEd0T0RJeG5x
dnRtN2RTeEs1andrVk9XUTZtMklDdmx5aEhrCmlNR1puUHVGaFYrUHNHYU1hbnpz
Q2J2ditrY1FuTXg3MUsvWm05WFcrUVUKLS0tIHdqeGFvNnkvVkdPWUphSkhETUpi
R1RWWEpDVkdHamx3M1p5a2YwUkR2REEKp/sZFcCNeZnHCvpwcOu827HLg0wxLWJG
EXHzgm5lZgiA/pQgxOJCiaz9HoNn3ttVE8z3aCnShUqAoXzpnh1Rpw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-21T20:22:40Z"
mac: ENC[AES256_GCM,data:wlXAnMwySlqHadgeAtgC+QCceqSw+mODWzKrqOTb4JSqvK1aEbPDUJWn8ow+0ff5OcU/3XVBr6b9GVzD6DsPXhg/aLsM/d5h6BRFb/qmrusD5YRyScu/gokwiD52I3qDf7rHeLNsYmoO4I6KuwEQRXasJ6H+QIOTNUxbcd+TTnM=,iv:8dNbZg7oJODgdR7mT3XgKL7sXooNYAvr8GM2l0DK6ao=,tag:M2JPh6qkisTG+A+2DCgABA==,type:str]
encrypted_regex: ^(data|stringData|email)$
version: 3.12.2

View File

@@ -0,0 +1,26 @@
apiVersion: v1
kind: Secret
metadata:
name: firefly-credentials
namespace: firefly
stringData:
APP_KEY: ENC[AES256_GCM,data:OugBKeVQqUDf8rFrGxMYgNROPesgVLsoKuQmWVvW9Kg=,iv:hnASzIOuoLczCUf0Luq2d6YXz5sKoHHawSys29cCMaE=,tag:u3SeTEc0VIb/NOwnXTn9Vg==,type:str]
STATIC_CRON_TOKEN: ENC[AES256_GCM,data:YL+ghixRjb1t6K1Q09clyxlYeYSj3rsy82n78efFVAw=,iv:V2q7uKOpl3e/FROZFvH7YCXBSUnQm7QcwqFyZaBbx2I=,tag:dZ1XPxEOGgAvfyL426rDTw==,type:str]
DB_USERNAME: ENC[AES256_GCM,data:Vk4jFf4Znw==,iv:LpP9DqkC1IlAwFNak8MTLPLf75iEpDwEm9ReLZPxRJo=,tag:qMexrfvQONJjfG11xh07wA==,type:str]
DB_PASSWORD: ENC[AES256_GCM,data:SsGb3qXMiq7EqGyfU2E6xqku1HbmWM8C,iv:+3V3Fh76gim9wcrhYd6CX7smyz0Kc8Qywx5j0wrmVaw=,tag:UawliAt7zOD0rpxNe3KcPw==,type:str]
DB_DATABASE: ENC[AES256_GCM,data:tLd4MWLoqQ==,iv:sG65BMBOAaKqYgCZ/xpv7LYaDNAPX+9KAeOBvYfUbgw=,tag:tRKptDmGV3iwV6luVFv/wQ==,type:str]
sops:
age:
- recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWdDRxUU1walNTNWY1VlVj
MVB3OFo2Nm03b3haTjFtV09TaWtuYnJXNEN3CmFFOUM4czgxWGFubUJrNDRpOTBq
eHB0ckJNZnltMC84S2NyQVRmUWNNMjQKLS0tIERUL0hwSzVmYlRHSVJwVnE0bkVt
Zm1kOVhMU0JOa1U1MzN2M2FhdlVYUG8KhOhnu/8FxuEJdW5O0HeYVCu5eLgeyqaN
Q88TjwbcwcIruXo/e0ATxWRp+yzwGB0nspQHZzAMP16uN6r3gZo3Zw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-21T20:06:48Z"
mac: ENC[AES256_GCM,data:4oJqb0OqFErl/Y3ydhrVYJRsaV4RVdppI6u+kpY03GgnStl06jEH/+puRnbh2/aemmh4qgZnt5zL9aW3ZXVeliW7R7vv9Ito7GMNkoFY0pwgqJqgTJfir9XjufqwTh7pOE6micajS9WKvvOKLzRe22uNiojzOpRy3M7f64QQ4R0=,iv:oTWMIaTjjRZCZsjJJqRX/H7ouFNjKGZdKjq94QFTtss=,tag:vVY6NkgUgqdjcjl2/rzycQ==,type:str]
encrypted_regex: ^(data|stringData|email)$
version: 3.12.2

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: firefly-db
namespace: firefly
spec:
clusterIP: None
selector:
app: firefly-db
ports:
- port: 5432
targetPort: 5432

View File

@@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: firefly-db
namespace: firefly
labels:
app: firefly-db
spec:
serviceName: firefly-db
replicas: 1
selector:
matchLabels:
app: firefly-db
template:
metadata:
labels:
app: firefly-db
spec:
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
containers:
- name: postgres
image: postgres:17
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: firefly-credentials
key: DB_USERNAME
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: firefly-credentials
key: DB_PASSWORD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: firefly-credentials
key: DB_DATABASE
- 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

View File

@@ -33,6 +33,7 @@ stringData:
CRYPTPAD_DATA_NFS_PATH: ENC[AES256_GCM,data:dNOxsmge//CgcBO0GIQUdcIBfaiGI1IVxXJ8IjmQl+Wh3A==,iv:hfuLuSWuSnbiItweWBSKxpxZJFgAWgUJCZNLcLZQVgw=,tag:7o353kHezQVRqb5BtC7rYw==,type:str]
CRYPTPAD_CONFIG_NFS_PATH: ENC[AES256_GCM,data:VJ4h7ADenNgFIiNIFK7pJKMrUBYc4e9c4MdVzqGoR4TDWGYL,iv:8ZhMHiZLh2C4J/vh/8L96R6VIkKoWc7ib/bUAQ5rZE0=,tag:HgvJjjYybCgxatUbcRFY7Q==,type:str]
SEERR_HOST: ENC[AES256_GCM,data:l64ttp+rLNU8GfwIE4fhJROSMDEb,iv:1vHOw0LyGN9OMhYemhtRq9GE2fc4J2EprZU3bp/h4kk=,tag:WNV0Jh7816ra7IIOBMspBg==,type:str]
FIREFLY_HOST: ENC[AES256_GCM,data:EQdrW33PVlD/brZCqh/sTkqLdPWW/bo=,iv:zbNnjsT1J6lpCEWtvNyL4A3bjWsusCjI5goWcZ8hajk=,tag:VwA6YA056bsyYIKB+X59bw==,type:str]
BACKUP_LOCAL_HOST: ENC[AES256_GCM,data:ABaTI3NKkhF7K2FpPwvvrHA0l/tCxAi4Qek=,iv:34ixxSpKU1c12uoMdk4nz2Vo/+5A/npB7NWMsWFytIM=,tag:qjw2fQBebCKnqCnAPiBHaw==,type:str]
sops:
age:
@@ -45,7 +46,7 @@ sops:
MGJ6TFpwR0diNjlEN2syZkhNMFNwRDQK9pzmQGB0GQu6ogMIJW+kugvBNj3w+dxW
bfEF9GAznIM/N5rPytF4wNgqwfoAF7GwumgA+iD43wprKtUJn+6dqw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-24T07:43:54Z"
mac: ENC[AES256_GCM,data:HDM/kZW9TxuIN6FBpE2kIdac2IU3yam807hfOAMRmX5l9DgaItPHmW6s/mSV2Mb9jDmWB1LjPipVcgPGdU/Lr4V6QcVPsaf+/KA73kAw4BBgiBOPFwBS0gdqGKfXbHS9uh3BTtjenvFnDD+tTnBsEOHSYjvfYFucE4A1GQc2EzU=,iv:lBEz0ayGP7dyNZ6YMIjpHMg2c11YdCWGpC7QLfTxy+8=,tag:nWCffX0iISgld7uGcdN9EQ==,type:str]
lastmodified: "2026-04-21T20:29:36Z"
mac: ENC[AES256_GCM,data:kiPibnbuR2b2nsniS7Y/2W7gH/z6M7Ay1Nx4D7A/qcP55udjHc3C+TWRnIoy/UYWV6G2g9YfHmywyhZI9x8xMG9NoQUZ14OYeFmjDGT48GV0NvZRIVcfIjzJRyzMI+pL8Fekn9HHSiYKBhjYLS0v/q/M3/ilA53tj3/90Gfinzk=,iv:AsJAmfBERd4BqQ4kn57UbFkFX4GAp+pZWt7WwteKKbw=,tag:RVD9mCEHxyhwh8VijAQmVQ==,type:str]
encrypted_regex: ^(data|stringData|email)$
version: 3.12.1
version: 3.12.2

File diff suppressed because one or more lines are too long