Skip to content

Self-hosted Personal Dis-Organizer with Vikunja

There is always too much to do and too little time, it never gets all done so the real challenge is to get the right things done at the right time, just to avoid Unforseen Consequences...

In one more attempt at getting better organized than having lots of browser tabs always open, emails unread in the inbox and, worst of all, uncountable ideas floating in the brain with nowhere to rest, lets try Vikunja, the fluffy, open-source, self-hostable to-do app.

Installation

Installing instructions include a basic example to run with docker and links to other setups. Vikunja Helm Chart includes are more complete setup, but for simplicity this time round Vikunja was deployed with this manifest:

vikunja.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: vikunja
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vikunja-data
  namespace: vikunja
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn-nvme
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vikunja
  namespace: vikunja
spec:
  replicas: 1
  revisionHistoryLimit: 0
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: vikunja
  template:
    metadata:
      labels:
        app: vikunja
    spec:
      securityContext:
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      containers:
      - name: vikunja
        image: vikunja/vikunja:1.0.0
        ports:
        - containerPort: 3456
        env:
        - name: VIKUNJA_SERVICE_JWTSECRET
          value: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        - name: VIKUNJA_DATABASE_TYPE
          value: "sqlite"
        - name: VIKUNJA_DATABASE_PATH
          value: "/app/vikunja/files/vikunja.db"
        - name: VIKUNJA_SERVICE_PUBLICURL
          value: "https://vikunja.very-very-dark-gray.top"
        volumeMounts:
        - name: data
          mountPath: /app/vikunja/files
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: vikunja-data
---
apiVersion: v1
kind: Service
metadata:
  name: vikunja
  namespace: vikunja
spec:
  selector:
    app: vikunja
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3456
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vikunja-ingress-tailscale
  namespace: vikunja
spec:
  ingressClassName: tailscale
  defaultBackend:
    service:
      name: vikunja
      port:
        number: 80
  tls:
    - hosts:
        - vikunja

This deployment includes an Ingress using the Tailscale kubernetes operator because the mobile app seems to be unable to have its requests authenticated through Pomerium, even after singing in using its embeded Chrome browser. To keep access to the full web UI secured behind Pomerium, this ingress is added to Pomerium Kustomize setup:

pomerium/pomerium-ingress/vikunja.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vikunja-pomerium-ingress
  namespace: vikunja
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    ingress.pomerium.io/allow_websockets: true
    ingress.pomerium.io/idle_timeout: 0s
    ingress.pomerium.io/pass_identity_headers: true
    ingress.pomerium.io/preserve_host_header: true
    ingress.pomerium.io/timeout: 0s
    ingress.pomerium.io/set_request_headers: |
      X-Forwarded-Proto: "https"
      X-Forwarded-Host: "vikunja.very-very-dark-gray.top"
spec:
  ingressClassName: pomerium
  rules:
    - host: vikunja.very-very-dark-gray.top
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: vikunja
                port:
                  number: 80
  tls:
    - secretName: tls-secret
      hosts:
        - vikunja.very-very-dark-gray.top

Applying this manifest should have the service up and running within the minute:

$ kubectl apply -f vikunja.yaml 
namespace/vikunja created
persistentvolumeclaim/vikunja-data created
deployment.apps/vikunja created
service/vikunja created

$ kubectl -n vikunja get all
NAME                          READY   STATUS    RESTARTS   AGE
pod/vikunja-75c7776d7-f7njz   1/1     Running   0          33s

NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/vikunja   ClusterIP   10.101.73.47   <none>        80/TCP    19m

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/vikunja   1/1     1            1           19m

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/vikunja-75c7776d7   1         1         1       33s

Applying the Pomerium Ingress needs a couple of minutes for the DNS challenge to be done:

$ kubectl apply -k pomerium/pomerium-ingress
...
ingress.networking.k8s.io/vikunja-pomerium-ingress created

$ kubectl get certificate -n vikunja -w
NAME         READY   SECRET       AGE
tls-secret   False   tls-secret   9s
tls-secret   False   tls-secret   97s

Once this is ready, the web UI is ready at https://vikunja.very-very-dark-gray.top

Android app

The Vikunja Android app is easy to install from the Play Store but not so easy to sing in with, when the web UI is secured behind SSO. There is not a separate path for the application to talk to the API, so to enable direct HTTPS access from the app it would be necessary to disable SSO ACLs on the web UI; not necessarily desirable.

Instead, the app can connect to the server via a Tailscale Ingress.

Moreover, the latest version of the mobile app is compatible only with version 1.0.0 of the server, which is why the above manifests deploys that old version instead of the latest (1.1.0).

On the other hand, the web UI can be accessed with a mobile browser (e.g. Firefox); time will tell whether the mobile app makes itself worthy all the above workarounds.