# 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: ```bash apps/vikunja/generate-secret.sh ``` This script: 1. generates a strong random secret (`openssl rand -hex 64`), 2. builds the Secret with `kubectl create ... --dry-run=client` (nothing touches the cluster), 3. pipes it through `kubeseal` using this cluster's controller (`--controller-name sealed-secrets-controller --controller-namespace kube-system`), 4. 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: ```bash 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: 1. commit `apps/vikunja/*` and `argocd/vikunja.yaml` to `main`, 2. push, 3. apply the Application once: `kubectl apply -f argocd/vikunja.yaml`, 4. Argo CD syncs the app, creates the `vikunja` namespace, deploys everything, and continuously self-heals it back to the Git state (prunes removed resources too). ## Verification ```bash 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: 1. Edit `apps/vikunja/vikunja-deployment.yaml` 2. Set `VIKUNJA_SERVICE_ENABLEREGISTRATION` to `"false"` 3. Commit and push 4. 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 ```bash # 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 sets `USER 1000`); the pod sets `runAsUser/runAsGroup/fsGroup: 1000` so the `local-path`-provisioned volumes are handed to group 1000 and remain writable. If you ever see "permission denied" writing `/db/vikunja.db`, verify the PVCs are `Bound` and the fsGroup is applied (`kubectl -n vikunja describe pod`). - SQLite + `Recreate` strategy: only one pod runs at a time, so the old pod never writes the database while the new one migrates/launches. - The `local-path` storage class has `reclaimPolicy: Delete` — deleting a PVC destroys the database/files. Back up `/db` and `/app/vikunja/files` (see `apps/sealed-secrets` backups for the general DR posture; adapt for vikunja volumes).