Skip to content

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:

sh
kubectl create namespace resticgui
kubectl -n resticgui create secret generic resticgui-key \
  --from-literal=key="$(openssl rand -base64 32)"

2. Apply the manifests ​

sh
kubectl apply -k 'https://github.com/mathieuHa/resticgui//deploy/kubernetes?ref=main'
kubectl -n resticgui rollout status deployment/resticgui

They create:

yaml
# 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:

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 ​

sh
kubectl -n resticgui port-forward service/resticgui 3001:80

Open 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:

sh
git clone https://github.com/mathieuHa/resticgui
cp -r resticgui/deploy/kubernetes my-resticgui
yaml
# 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}
sh
kubectl apply -k my-resticgui

A 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:

sh
kubectl -n resticgui exec -it deploy/resticgui -- /opt/resticgui hash-password
yaml
# 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:

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: latest

then kubectl apply -k again. The Recreate strategy stops the old pod before the new one opens the database.

Released under the GPL-3.0 license.