On Kubernetes
ResticGUI runs as one pod: it keeps its state in SQLite, on a PersistentVolumeClaim. The manifests are in deploy/kubernetes, ready for kubectl apply -k.
1. Create the namespace and the key
ResticGUI encrypts the repository passwords and credentials it saves with a key. Keep it in a Secret, and a copy somewhere safe:
kubectl create namespace resticgui
kubectl -n resticgui create secret generic resticgui-key \
--from-literal=key="$(openssl rand -base64 32)"2. Apply the manifests
kubectl apply -k 'https://github.com/mathieuHa/resticgui//deploy/kubernetes?ref=main'
kubectl -n resticgui rollout status deployment/resticguiThey create:
# ResticGUI on Kubernetes: one replica (it keeps its state in SQLite),
# its database on a PersistentVolumeClaim, the encryption key in a Secret.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: resticgui-db
labels: {app.kubernetes.io/name: resticgui}
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: resticgui
labels: {app.kubernetes.io/name: resticgui}
spec:
replicas: 1
# never two pods on one database
strategy: {type: Recreate}
selector:
matchLabels: {app.kubernetes.io/name: resticgui}
template:
metadata:
labels: {app.kubernetes.io/name: resticgui}
spec:
securityContext:
seccompProfile: {type: RuntimeDefault}
containers:
- name: resticgui
image: ghcr.io/mathieuha/resticgui:latest
# root, to read the files of every volume it backs up and restore
# them with their owners, but with only those rights
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
add: [CHOWN, DAC_OVERRIDE, DAC_READ_SEARCH, FOWNER, FSETID]
ports:
- {name: http, containerPort: 3030}
env:
- name: RESTICGUI_SECRET_KEY
valueFrom:
secretKeyRef: {name: resticgui-key, key: key}
- {name: RESTIC_CACHE_DIR, value: /opt/db/cache}
# schedules follow this time zone
- {name: TZ, value: UTC}
volumeMounts:
- {name: db, mountPath: /opt/db}
- {name: config, mountPath: /opt/config.yaml, subPath: config.yaml, readOnly: true}
- {name: tmp, mountPath: /tmp}
# what to back up, for example a volume of another application:
# - {name: data, mountPath: /backup/data, readOnly: true}
readinessProbe:
httpGet: {path: /api/v2/version, port: http}
periodSeconds: 10
livenessProbe:
httpGet: {path: /api/v2/version, port: http}
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests: {cpu: 50m, memory: 128Mi}
limits: {memory: 1Gi}
volumes:
- name: db
persistentVolumeClaim: {claimName: resticgui-db}
- name: config
configMap: {name: resticgui-config}
- name: tmp
emptyDir: {}
# - name: data
# persistentVolumeClaim: {claimName: my-app-data}
---
apiVersion: v1
kind: Service
metadata:
name: resticgui
labels: {app.kubernetes.io/name: resticgui}
spec:
selector: {app.kubernetes.io/name: resticgui}
ports:
- {name: http, port: 80, targetPort: http}
---
# Nothing in the cluster may connect to ResticGUI: reach it with
# kubectl port-forward, or allow your ingress controller (see ingress.yaml).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: resticgui
labels: {app.kubernetes.io/name: resticgui}
spec:
podSelector:
matchLabels: {app.kubernetes.io/name: resticgui}
policyTypes: [Ingress]
ingress: []and a ConfigMap from this configuration, mounted at /opt/config.yaml:
# ResticGUI's settings, mounted at /opt/config.yaml.
# All of them: https://docs.resticgui.com/reference/configuration
server:
ip: 0.0.0.0
port: 3030
store:
path: /opt/db/resticgui.sqlite
# the key comes from the resticgui-key Secret ($RESTICGUI_SECRET_KEY)
key_file: /opt/db/resticgui.key
legacy_path: ""
restic:
bin: /usr/bin/restic
paths:
# what the pod may back up and restore into: the volumes mounted under /backup
roots: [/backup]
auth:
username: admin
# resticgui hash-password; required as soon as the Service is reachable by others
password_hash: ""3. Open it
kubectl -n resticgui port-forward service/resticgui 3001:80Open http://127.0.0.1:3001/ui/ and continue with your first backup.
The manifests keep ResticGUI to itself: a NetworkPolicy refuses connections from other pods (port-forward still works), the container's root filesystem is read-only, and it runs as root with only the capabilities needed to read and restore files of every owner. Mount what to back up under /backup, as below.
Back up the volumes of an application
Mount the volumes under /backup, read-only, in the Deployment. Copy the folder, change it, and apply your copy:
git clone https://github.com/mathieuHa/resticgui
cp -r resticgui/deploy/kubernetes my-resticgui# my-resticgui/resticgui.yaml, in the container
volumeMounts:
- {name: db, mountPath: /opt/db}
- {name: config, mountPath: /opt/config.yaml, subPath: config.yaml, readOnly: true}
- {name: data, mountPath: /backup/data, readOnly: true}
# and in the pod
volumes:
- name: data
persistentVolumeClaim: {claimName: my-app-data}kubectl apply -k my-resticguiA ReadWriteOnce volume can be mounted by pods on one node at a time: put ResticGUI on the node of the application, with a nodeSelector or pod affinity, or use a ReadWriteMany volume.
Ingress and a password
Make a password hash, put it in config.yaml, and add ingress.yaml to the resources of kustomization.yaml:
kubectl -n resticgui exec -it deploy/resticgui -- /opt/resticgui hash-password# Optional: ResticGUI behind an Ingress, with TLS. Set auth.password_hash
# in config.yaml first. Add it to the resources of kustomization.yaml.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: resticgui
labels: {app.kubernetes.io/name: resticgui}
annotations:
# live progress is a server-sent event stream: no buffering
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
spec:
rules:
- host: backup.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: {name: resticgui, port: {name: http}}
tls:
- hosts: [backup.example.com]
secretName: resticgui-tls
---
# lets the ingress controller through the NetworkPolicy of resticgui.yaml;
# change the namespace to your controller's
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: resticgui-from-ingress
labels: {app.kubernetes.io/name: resticgui}
spec:
podSelector:
matchLabels: {app.kubernetes.io/name: resticgui}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: ingress-nginx}
ports:
- {port: http}Its NetworkPolicy lets the ingress controller through; change the namespace if yours is not ingress-nginx. With a host name and no password, ResticGUI answers only if the name is in server.allowed_hosts.
Updates
The manifests use the latest tag. Pin a version in kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: resticgui
resources:
- resticgui.yaml
# - ingress.yaml
configMapGenerator:
- name: resticgui-config
files: [config.yaml]
images:
- name: ghcr.io/mathieuha/resticgui
newTag: latestthen kubectl apply -k again. The Recreate strategy stops the old pod before the new one opens the database.