5.7 KiB
Vikunja
Self-hosted to-do / task management (Vikunja 2.6.0), served on the internal network.
URL
- Web:
https://vikunja.aleshym.co
Internal DNS
Pi-hole local DNS record (create it manually if not present):
vikunja.aleshym.co A 10.10.0.100
10.10.0.100 is the k3s node and the traefik LoadBalancer's external IP — the same
destination Vaultwarden uses for vault.aleshym.co.
Architecture
This app is private (LAN/VPN only, no public WAN exposure), exactly like
Vaultwarden. It is deliberately absent from the k3s catchall-to-docker-apps public
host list and from the Docker Traefik wan entrypoint. Pi-hole resolves the hostname to
the node IP, and Traefik terminates TLS for it.
Pi-hole DNS
|
v
10.10.0.100 (k3s node / Traefik LoadBalancer external IP)
|
Traefik (ingressClassName traefik, websecure entrypoint, letsencrypt-prod)
|
Vikunja Service (ClusterIP, port 80 -> pod http)
|
Vikunja Pod :3456
|
+-- SQLite PVC (/db/vikunja.db)
|
+-- Files PVC (/app/vikunja/files)
No NodePort / hostNetwork / separate LoadBalancer is used for Vikunja; Traefik remains the single ingress point.
Secret generation
The service secret (VIKUNJA_SERVICE_SECRET) must come from a Kubernetes Secret named
vikunja-secret in the vikunja namespace, which is generated from a SealedSecret.
The encrypted sealed secret is not committed in plaintext form.
Generate the real apps/vikunja/vikunja-sealedsecret.yaml from the repo root:
apps/vikunja/generate-secret.sh
This script:
- generates a strong random secret (
openssl rand -hex 64), - builds the Secret with
kubectl create ... --dry-run=client(nothing touches the cluster), - pipes it through
kubesealusing this cluster's controller (--controller-name sealed-secrets-controller --controller-namespace kube-system), - writes the encrypted manifest to
apps/vikunja/vikunja-sealedsecret.yaml.
It never prints the secret. Commit the generated file before/alongside the Argo CD sync so Vikunja gets a stable JWT-signing secret across restarts. Equivalent manual one-liner:
SECRET="$(openssl rand -hex 64)"
kubectl create secret generic vikunja-secret \
--namespace vikunja \
--from-literal=VIKUNJA_SERVICE_SECRET="$SECRET" \
--dry-run=client -o yaml \
| kubeseal \
--controller-name sealed-secrets-controller \
--controller-namespace kube-system \
--format yaml \
--namespace vikunja \
--name vikunja-secret \
> apps/vikunja/vikunja-sealedsecret.yaml
unset SECRET
The controller's private key must be restored on the cluster before unsealing works (see
apps/sealed-secrets/README.md).
Deployment
argocd/vikunja.yaml registers an Argo CD Application pointing at:
repoURL: https://git.aleshym.co/funman300/k3s-homelab.git
targetRevision: main
path: apps/vikunja
destination: https://kubernetes.default.svc / namespace vikunja
automated: prune: true, selfHeal: true
syncOptions: CreateNamespace=true
Flow:
- commit
apps/vikunja/*andargocd/vikunja.yamltomain, - push,
- apply the Application once:
kubectl apply -f argocd/vikunja.yaml, - Argo CD syncs the app, creates the
vikunjanamespace, deploys everything, and continuously self-heals it back to the Git state (prunes removed resources too).
Verification
kubectl -n vikunja get all
kubectl -n vikunja get pvc
kubectl -n vikunja get ingress
kubectl -n vikunja describe deployment vikunja
kubectl -n vikunja logs deployment/vikunja
curl -k https://vikunja.aleshym.co/health
Registration
Registration is enabled initially so the first account can be created at
https://vikunja.aleshym.co. It is controlled by:
- Config key:
service.enableregistration - Environment variable:
VIKUNJA_SERVICE_ENABLEREGISTRATION
It is set to "true" in apps/vikunja/vikunja-deployment.yaml. After creating your
account, disable new registrations:
- Edit
apps/vikunja/vikunja-deployment.yaml - Set
VIKUNJA_SERVICE_ENABLEREGISTRATIONto"false" - Commit and push
- Argo CD (selfHeal) syncs the new Deployment and the Recreate strategy rolls the pod
Once disabled, additional accounts can still be created via the CLI
(vikunja user create), e.g. in a temporary pod mounting the same PVC.
Troubleshooting
# pod status / events / logs
kubectl -n vikunja get pods -o wide
kubectl -n vikunja get events --sort-by=.lastTimestamp
kubectl -n vikunja logs deployment/vikunja
# PVC status (bound? local-path volume created?)
kubectl -n vikunja get pvc
kubectl -n vikunja describe pvc vikunja-db vikunja-files
# ingress + certificate (TLS via letsencrypt-prod)
kubectl -n vikunja describe ingress
kubectl -n vikunja get certificate
kubectl -n vikunja describe certificate
# sealed secret + generated secret
kubectl -n vikunja get sealedsecret
kubectl -n vikunja get sealedsecret vikunja-secret -o yaml
kubectl -n vikunja get secret vikunja-secret
SQLite / volume permissions
- Runtime user is UID/GID
1000(the official image setsUSER 1000); the pod setsrunAsUser/runAsGroup/fsGroup: 1000so thelocal-path-provisioned volumes are handed to group 1000 and remain writable. If you ever see "permission denied" writing/db/vikunja.db, verify the PVCs areBoundand the fsGroup is applied (kubectl -n vikunja describe pod). - SQLite +
Recreatestrategy: only one pod runs at a time, so the old pod never writes the database while the new one migrates/launches. - The
local-pathstorage class hasreclaimPolicy: Delete— deleting a PVC destroys the database/files. Back up/dband/app/vikunja/files(seeapps/sealed-secretsbackups for the general DR posture; adapt for vikunja volumes).