diff --git a/apps/notesnook/attachments-ingress.yaml b/apps/notesnook/attachments-ingress.yaml new file mode 100644 index 0000000..04de70e --- /dev/null +++ b/apps/notesnook/attachments-ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-attachments + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: attachments.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: notesnook-s3 + port: { number: 9000 } + tls: + - hosts: ["attachments.notes.aleshym.co"] + secretName: notesnook-attachments-tls diff --git a/apps/notesnook/db-init-job.yaml b/apps/notesnook/db-init-job.yaml new file mode 100644 index 0000000..bf01207 --- /dev/null +++ b/apps/notesnook/db-init-job.yaml @@ -0,0 +1,27 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: notesnook-db-init + namespace: notesnook + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 20 + template: + spec: + restartPolicy: OnFailure + containers: + - name: rs-init + image: mongo:7.0.12 + command: ["bash", "-c"] + args: + - | + until mongosh mongodb://notesnook-db:27017 --quiet --eval 'db.runCommand("ping").ok' ; do + echo "waiting for mongod..."; sleep 3; + done + mongosh mongodb://notesnook-db:27017 --quiet --eval ' + try { rs.status() } + catch (e) { rs.initiate({_id:"rs0", members:[{_id:0, host:"notesnook-db:27017"}]}) } + ' + echo "replica set ready" diff --git a/apps/notesnook/db-service.yaml b/apps/notesnook/db-service.yaml new file mode 100644 index 0000000..2d8cc6f --- /dev/null +++ b/apps/notesnook/db-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: notesnook-db + namespace: notesnook +spec: + clusterIP: None + selector: + app: notesnook-db + ports: + - name: mongo + port: 27017 + targetPort: 27017 diff --git a/apps/notesnook/db-statefulset.yaml b/apps/notesnook/db-statefulset.yaml new file mode 100644 index 0000000..5ad82d0 --- /dev/null +++ b/apps/notesnook/db-statefulset.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: notesnook-db + namespace: notesnook +spec: + serviceName: notesnook-db + replicas: 1 + selector: + matchLabels: + app: notesnook-db + template: + metadata: + labels: + app: notesnook-db + spec: + containers: + - name: mongod + image: mongo:7.0.12 + args: ["--replSet", "rs0", "--bind_ip_all"] + ports: + - containerPort: 27017 + name: mongo + volumeMounts: + - name: dbdata + mountPath: /data/db + readinessProbe: + exec: + command: ["mongosh", "--quiet", "--eval", "db.runCommand('ping').ok"] + initialDelaySeconds: 10 + periodSeconds: 15 + resources: + requests: { cpu: "100m", memory: "256Mi" } + limits: { cpu: "1000m", memory: "1Gi" } + volumeClaimTemplates: + - metadata: + name: dbdata + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi diff --git a/apps/notesnook/identity-deployment.yaml b/apps/notesnook/identity-deployment.yaml new file mode 100644 index 0000000..0937543 --- /dev/null +++ b/apps/notesnook/identity-deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-identity + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-identity + template: + metadata: + labels: + app: notesnook-identity + spec: + containers: + - name: identity + image: streetwriters/identity:latest + envFrom: + - configMapRef: { name: notesnook-config } + - secretRef: { name: notesnook-secret } + env: + - name: MONGODB_CONNECTION_STRING + value: "mongodb://notesnook-db:27017/identity?replSet=rs0" + - name: MONGODB_DATABASE_NAME + value: "identity" + ports: + - { containerPort: 8264, name: http } + readinessProbe: + httpGet: { path: /health, port: 8264 } + initialDelaySeconds: 20 + periodSeconds: 15 + livenessProbe: + httpGet: { path: /health, port: 8264 } + initialDelaySeconds: 60 + periodSeconds: 30 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } diff --git a/apps/notesnook/identity-ingress.yaml b/apps/notesnook/identity-ingress.yaml new file mode 100644 index 0000000..cb1f636 --- /dev/null +++ b/apps/notesnook/identity-ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-auth + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: auth.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: notesnook-identity + port: { name: http } + tls: + - hosts: ["auth.notes.aleshym.co"] + secretName: notesnook-auth-tls diff --git a/apps/notesnook/identity-service.yaml b/apps/notesnook/identity-service.yaml new file mode 100644 index 0000000..3a10bd6 --- /dev/null +++ b/apps/notesnook/identity-service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: notesnook-identity + namespace: notesnook +spec: + selector: + app: notesnook-identity + ports: + - { name: http, port: 8264, targetPort: 8264 } diff --git a/apps/notesnook/kustomization.yaml b/apps/notesnook/kustomization.yaml new file mode 100644 index 0000000..a33ee61 --- /dev/null +++ b/apps/notesnook/kustomization.yaml @@ -0,0 +1,26 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: notesnook +resources: + - namespace.yaml + - notesnook-config.yaml + - db-service.yaml + - db-statefulset.yaml + - db-init-job.yaml + - minio-pvc.yaml + - minio-deployment.yaml + - minio-service.yaml + - minio-bucket-job.yaml + - identity-deployment.yaml + - identity-service.yaml + - identity-ingress.yaml + - sync-deployment.yaml + - sync-service.yaml + - sync-ingress.yaml + - sse-deployment.yaml + - sse-service.yaml + - sse-ingress.yaml + - monograph-deployment.yaml + - monograph-service.yaml + - monograph-ingress.yaml + - attachments-ingress.yaml diff --git a/apps/notesnook/minio-bucket-job.yaml b/apps/notesnook/minio-bucket-job.yaml new file mode 100644 index 0000000..7e0ace5 --- /dev/null +++ b/apps/notesnook/minio-bucket-job.yaml @@ -0,0 +1,26 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: notesnook-s3-setup + namespace: notesnook + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 20 + template: + spec: + restartPolicy: OnFailure + containers: + - name: mc + image: minio/mc:RELEASE.2024-07-26T13-08-44Z + envFrom: + - secretRef: { name: notesnook-secret } + command: ["/bin/bash", "-c"] + args: + - | + until mc alias set minio http://notesnook-s3:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do + sleep 2; + done + mc mb --ignore-existing minio/attachments + echo "bucket ready" diff --git a/apps/notesnook/minio-deployment.yaml b/apps/notesnook/minio-deployment.yaml new file mode 100644 index 0000000..c64443c --- /dev/null +++ b/apps/notesnook/minio-deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-s3 + namespace: notesnook +spec: + replicas: 1 + strategy: { type: Recreate } + selector: + matchLabels: + app: notesnook-s3 + template: + metadata: + labels: + app: notesnook-s3 + spec: + containers: + - name: minio + image: minio/minio:RELEASE.2024-07-29T22-14-52Z + args: ["server", "/data/s3", "--console-address", ":9090"] + envFrom: + - secretRef: { name: notesnook-secret } + env: + - name: MINIO_BROWSER + value: "on" + ports: + - { containerPort: 9000, name: api } + - { containerPort: 9090, name: console } + volumeMounts: + - name: s3data + mountPath: /data/s3 + readinessProbe: + tcpSocket: { port: 9000 } + initialDelaySeconds: 10 + periodSeconds: 15 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } + volumes: + - name: s3data + persistentVolumeClaim: + claimName: notesnook-s3-data diff --git a/apps/notesnook/minio-pvc.yaml b/apps/notesnook/minio-pvc.yaml new file mode 100644 index 0000000..976e27b --- /dev/null +++ b/apps/notesnook/minio-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: notesnook-s3-data + namespace: notesnook +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 20Gi diff --git a/apps/notesnook/minio-service.yaml b/apps/notesnook/minio-service.yaml new file mode 100644 index 0000000..08c04c0 --- /dev/null +++ b/apps/notesnook/minio-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: notesnook-s3 + namespace: notesnook +spec: + selector: + app: notesnook-s3 + ports: + - { name: api, port: 9000, targetPort: 9000 } + - { name: console, port: 9090, targetPort: 9090 } diff --git a/apps/notesnook/monograph-deployment.yaml b/apps/notesnook/monograph-deployment.yaml new file mode 100644 index 0000000..f799008 --- /dev/null +++ b/apps/notesnook/monograph-deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-monograph + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-monograph + template: + metadata: + labels: + app: notesnook-monograph + spec: + containers: + - name: monograph + image: streetwriters/monograph:latest + envFrom: + - configMapRef: { name: notesnook-config } + - secretRef: { name: notesnook-secret } + env: + - name: HOST + value: "0.0.0.0" + - name: API_HOST + value: "http://notesnook-sync:5264" + - name: PUBLIC_URL + value: "https://monograph.notes.aleshym.co" + ports: + - { containerPort: 3000, name: http } + readinessProbe: + httpGet: { path: /, port: 3000 } + initialDelaySeconds: 20 + periodSeconds: 15 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } diff --git a/apps/notesnook/monograph-ingress.yaml b/apps/notesnook/monograph-ingress.yaml new file mode 100644 index 0000000..13b3723 --- /dev/null +++ b/apps/notesnook/monograph-ingress.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-monograph + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: monograph.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-monograph, port: { name: http } } + tls: + - hosts: ["monograph.notes.aleshym.co"] + secretName: notesnook-monograph-tls diff --git a/apps/notesnook/monograph-service.yaml b/apps/notesnook/monograph-service.yaml new file mode 100644 index 0000000..3082314 --- /dev/null +++ b/apps/notesnook/monograph-service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: notesnook-monograph + namespace: notesnook +spec: + selector: + app: notesnook-monograph + ports: + - { name: http, port: 3000, targetPort: 3000 } diff --git a/apps/notesnook/namespace.yaml b/apps/notesnook/namespace.yaml new file mode 100644 index 0000000..abd9fc9 --- /dev/null +++ b/apps/notesnook/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: notesnook diff --git a/apps/notesnook/notesnook-config.yaml b/apps/notesnook/notesnook-config.yaml new file mode 100644 index 0000000..4563773 --- /dev/null +++ b/apps/notesnook/notesnook-config.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: notesnook-config + namespace: notesnook +data: + INSTANCE_NAME: "aleshym-notesnook" + DISABLE_SIGNUPS: "true" + NOTESNOOK_APP_PUBLIC_URL: "https://app.notesnook.com" + AUTH_SERVER_PUBLIC_URL: "https://auth.notes.aleshym.co" + MONOGRAPH_PUBLIC_URL: "https://monograph.notes.aleshym.co" + ATTACHMENTS_SERVER_PUBLIC_URL: "https://attachments.notes.aleshym.co" + NOTESNOOK_CORS_ORIGINS: "https://app.notesnook.com,https://notes.aleshym.co" + KNOWN_PROXIES: "" + SELF_HOSTED: "1" + NOTESNOOK_SERVER_HOST: "notesnook-sync" + NOTESNOOK_SERVER_PORT: "5264" + IDENTITY_SERVER_HOST: "notesnook-identity" + IDENTITY_SERVER_PORT: "8264" + SSE_SERVER_HOST: "notesnook-sse" + SSE_SERVER_PORT: "7264" + IDENTITY_SERVER_URL: "https://auth.notes.aleshym.co" + NOTESNOOK_APP_HOST: "https://app.notesnook.com" + S3_INTERNAL_SERVICE_URL: "http://notesnook-s3:9000" + S3_INTERNAL_BUCKET_NAME: "attachments" + S3_SERVICE_URL: "https://attachments.notes.aleshym.co" + S3_BUCKET_NAME: "attachments" + S3_REGION: "us-east-1" + MINIO_BROWSER: "on" diff --git a/apps/notesnook/notesnook-secret.yaml.example b/apps/notesnook/notesnook-secret.yaml.example new file mode 100644 index 0000000..e492e11 --- /dev/null +++ b/apps/notesnook/notesnook-secret.yaml.example @@ -0,0 +1,19 @@ +# DO NOT COMMIT THE REAL VERSION. Copy to notesnook-secret.yaml (gitignored), +# fill real values from the old stack's .env, then: +# kubectl apply -f apps/notesnook/notesnook-secret.yaml +# ArgoCD ignores this Secret's /data (see argocd/notesnook.yaml). +apiVersion: v1 +kind: Secret +metadata: + name: notesnook-secret + namespace: notesnook +stringData: + NOTESNOOK_API_SECRET: "CHANGE_ME" + SMTP_USERNAME: "funman300@gmail.com" + SMTP_PASSWORD: "CHANGE_ME" + SMTP_HOST: "smtp.gmail.com" + SMTP_PORT: "587" + MINIO_ROOT_USER: "notesnook" + MINIO_ROOT_PASSWORD: "CHANGE_ME" + S3_ACCESS_KEY_ID: "notesnook" + S3_ACCESS_KEY: "CHANGE_ME" diff --git a/apps/notesnook/sse-deployment.yaml b/apps/notesnook/sse-deployment.yaml new file mode 100644 index 0000000..8709098 --- /dev/null +++ b/apps/notesnook/sse-deployment.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-sse + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-sse + template: + metadata: + labels: + app: notesnook-sse + spec: + containers: + - name: sse + image: streetwriters/sse:latest + envFrom: + - configMapRef: { name: notesnook-config } + - secretRef: { name: notesnook-secret } + ports: + - { containerPort: 7264, name: http } + readinessProbe: + httpGet: { path: /health, port: 7264 } + initialDelaySeconds: 20 + periodSeconds: 15 + livenessProbe: + httpGet: { path: /health, port: 7264 } + initialDelaySeconds: 60 + periodSeconds: 30 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "256Mi" } diff --git a/apps/notesnook/sse-ingress.yaml b/apps/notesnook/sse-ingress.yaml new file mode 100644 index 0000000..6258db3 --- /dev/null +++ b/apps/notesnook/sse-ingress.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-sse + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: events.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-sse, port: { name: http } } + tls: + - hosts: ["events.notes.aleshym.co"] + secretName: notesnook-sse-tls diff --git a/apps/notesnook/sse-service.yaml b/apps/notesnook/sse-service.yaml new file mode 100644 index 0000000..537db8f --- /dev/null +++ b/apps/notesnook/sse-service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: notesnook-sse + namespace: notesnook +spec: + selector: + app: notesnook-sse + ports: + - { name: http, port: 7264, targetPort: 7264 } diff --git a/apps/notesnook/sync-deployment.yaml b/apps/notesnook/sync-deployment.yaml new file mode 100644 index 0000000..35cb102 --- /dev/null +++ b/apps/notesnook/sync-deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-sync + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-sync + template: + metadata: + labels: + app: notesnook-sync + spec: + containers: + - name: sync + image: streetwriters/notesnook-sync:latest + envFrom: + - configMapRef: { name: notesnook-config } + - secretRef: { name: notesnook-secret } + env: + - name: MONGODB_CONNECTION_STRING + value: "mongodb://notesnook-db:27017/?replSet=rs0" + - name: MONGODB_DATABASE_NAME + value: "notesnook" + ports: + - { containerPort: 5264, name: http } + readinessProbe: + httpGet: { path: /health, port: 5264 } + initialDelaySeconds: 20 + periodSeconds: 15 + livenessProbe: + httpGet: { path: /health, port: 5264 } + initialDelaySeconds: 60 + periodSeconds: 30 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "1000m", memory: "512Mi" } diff --git a/apps/notesnook/sync-ingress.yaml b/apps/notesnook/sync-ingress.yaml new file mode 100644 index 0000000..8772fbe --- /dev/null +++ b/apps/notesnook/sync-ingress.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-sync + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: sync.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-sync, port: { name: http } } + - host: notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-sync, port: { name: http } } + tls: + - hosts: ["sync.notes.aleshym.co", "notes.aleshym.co"] + secretName: notesnook-sync-tls diff --git a/apps/notesnook/sync-service.yaml b/apps/notesnook/sync-service.yaml new file mode 100644 index 0000000..2815d1d --- /dev/null +++ b/apps/notesnook/sync-service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: notesnook-sync + namespace: notesnook +spec: + selector: + app: notesnook-sync + ports: + - { name: http, port: 5264, targetPort: 5264 } diff --git a/argocd/notesnook.yaml b/argocd/notesnook.yaml new file mode 100644 index 0000000..d5f03f6 --- /dev/null +++ b/argocd/notesnook.yaml @@ -0,0 +1,27 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: notesnook + namespace: argocd +spec: + project: default + source: + repoURL: https://git.aleshym.co/funman300/k3s-homelab.git + targetRevision: main + path: apps/notesnook + destination: + server: https://kubernetes.default.svc + namespace: notesnook + ignoreDifferences: + - group: "" + kind: Secret + name: notesnook-secret + namespace: notesnook + jsonPointers: + - /data + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/docs/superpowers/plans/2026-07-15-notesnook-k8s-migration.md b/docs/superpowers/plans/2026-07-15-notesnook-k8s-migration.md new file mode 100644 index 0000000..de77057 --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-notesnook-k8s-migration.md @@ -0,0 +1,1192 @@ +# Notesnook → k3s Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the self-hosted Notesnook stack from docker-compose on `10.10.0.5` to the k3s cluster (`apps/notesnook/` in the `k3s-homelab` GitOps repo), preserving the existing account, notes, keys, and attachments. + +**Architecture:** Replicate the compose stack as k8s workloads — a single-node MongoDB replica set (`rs0`), MinIO (S3), and the four streetwriters services (identity, sync, sse, monograph) — sharing one ConfigMap (non-secret env, mirroring compose's `env_file: .env` + `server-discovery` anchor) and one out-of-band Secret. Traefik ingress per public host with cert-manager (Cloudflare DNS-01) TLS. Data is copied via `mongodump`/`mongorestore` and `mc mirror` during a brief write-frozen cutover, then internal DNS on `10.10.0.8` is repointed from `10.10.0.5` to `10.10.0.100`. + +**Tech Stack:** k3s v1.35, ArgoCD, Kustomize, Traefik, cert-manager, MongoDB 7.0.12, MinIO, streetwriters/{identity,notesnook-sync,sse,monograph}. + +## Global Constraints + +- Namespace: `notesnook`. Repo path: `apps/notesnook/`. ArgoCD app: `argocd/notesnook.yaml`. +- Image pins (match compose exactly): `mongo:7.0.12`, `minio/minio:RELEASE.2024-07-29T22-14-52Z`, `minio/mc:RELEASE.2024-07-26T13-08-44Z`, `streetwriters/identity:latest`, `streetwriters/notesnook-sync:latest`, `streetwriters/sse:latest`, `streetwriters/monograph:latest`. +- **Do NOT regenerate `NOTESNOOK_API_SECRET` or MinIO credentials** — reuse the existing values from the old `.env`; migrated data depends on them. +- Secrets are applied out-of-band: commit only `notesnook-secret.yaml.example`; the real `apps/notesnook/notesnook-secret.yaml` is gitignored (`apps/**/*-secret.yaml` already covered). +- Ingress convention (copy from `apps/nightscout/nightscout-ingress.yaml`): `ingressClassName: traefik`, annotations `cert-manager.io/cluster-issuer: letsencrypt-prod` and `traefik.ingress.kubernetes.io/router.entrypoints: websecure`, per-host `tls.secretName`. +- Public hosts: `auth`/`sync`/`events`/`monograph`/`attachments`.notes.aleshym.co + apex `notes.aleshym.co`. All resolve (post-cutover) to `10.10.0.100` (Traefik LB). +- In-cluster service names (replace compose hostnames): `notesnook-db`, `notesnook-s3`, `notesnook-identity`, `notesnook-sync`, `notesnook-sse`, `notesnook-monograph`. +- `KNOWN_PROXIES=10.42.0.0/16` (k3s pod CIDR; node podCIDR observed `10.42.0.0/24` — /16 covers the cluster). Verify in Task 3 and adjust if forwarded-header/client-IP handling misbehaves. +- **Testing strategy:** build on a git branch; validate runtime by applying directly with `kubectl apply -k apps/notesnook` (ArgoCD watches `main` only, so it won't interfere on the branch). After end-to-end validation, merge to `main`; ArgoCD adopts the already-running resources (identical manifests → no churn). + +## Access prerequisites (resolve before Task 9 / Task 11) + +- **Git push** to `git.aleshym.co/funman300/k3s-homelab` (current clone is read-only). Default workflow: feature branch `feat/notesnook-k8s` → PR → merge to `main`. +- **Internal DNS edit** on `10.10.0.8` (Task 11 cutover). Identify the service (AdGuard/Pi-hole/unbound) and access method. +- Old-stack values needed for the real Secret (Task 9): pull from `manage@10.10.0.5:~/notesnook/.env` — `NOTESNOOK_API_SECRET`, `SMTP_PASSWORD`, `MINIO_ROOT_PASSWORD`. + +## File Structure + +``` +apps/notesnook/ + kustomization.yaml + namespace.yaml + notesnook-config.yaml # ConfigMap: all non-secret env + server-discovery + notesnook-secret.yaml.example # Secret template (real applied out-of-band) + db-statefulset.yaml # mongo:7.0.12 --replSet rs0 + db-service.yaml # headless Service notesnook-db + db-init-job.yaml # idempotent rs.initiate() + minio-pvc.yaml + minio-deployment.yaml + minio-service.yaml + minio-bucket-job.yaml # create 'attachments' bucket + identity-deployment.yaml + identity-service.yaml + identity-ingress.yaml + sync-deployment.yaml + sync-service.yaml + sync-ingress.yaml + sse-deployment.yaml + sse-service.yaml + sse-ingress.yaml + monograph-deployment.yaml + monograph-service.yaml + monograph-ingress.yaml + attachments-ingress.yaml +argocd/notesnook.yaml +``` + +--- + +### Task 1: Scaffold namespace, ConfigMap, secret template, kustomization + +**Files:** +- Create: `apps/notesnook/namespace.yaml`, `apps/notesnook/notesnook-config.yaml`, `apps/notesnook/notesnook-secret.yaml.example`, `apps/notesnook/kustomization.yaml` + +**Interfaces:** +- Produces: ConfigMap `notesnook-config` and Secret `notesnook-secret` (both in ns `notesnook`), consumed via `envFrom` by every service Deployment in later tasks. + +- [ ] **Step 1: Create the branch** + +```bash +cd +git checkout -b feat/notesnook-k8s +``` + +- [ ] **Step 2: namespace.yaml** + +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: notesnook +``` + +- [ ] **Step 3: notesnook-config.yaml** (non-secret env — mirrors compose `.env` non-secrets + `server-discovery`, with k8s service hostnames) + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: notesnook-config + namespace: notesnook +data: + INSTANCE_NAME: "aleshym-notesnook" + DISABLE_SIGNUPS: "true" + NOTESNOOK_APP_PUBLIC_URL: "https://app.notesnook.com" + AUTH_SERVER_PUBLIC_URL: "https://auth.notes.aleshym.co" + MONOGRAPH_PUBLIC_URL: "https://monograph.notes.aleshym.co" + ATTACHMENTS_SERVER_PUBLIC_URL: "https://attachments.notes.aleshym.co" + NOTESNOOK_CORS_ORIGINS: "https://app.notesnook.com,https://notes.aleshym.co" + KNOWN_PROXIES: "10.42.0.0/16" + # server-discovery (compose anchor) — hostnames point at k8s Services + SELF_HOSTED: "1" + NOTESNOOK_SERVER_HOST: "notesnook-sync" + NOTESNOOK_SERVER_PORT: "5264" + IDENTITY_SERVER_HOST: "notesnook-identity" + IDENTITY_SERVER_PORT: "8264" + SSE_SERVER_HOST: "notesnook-sse" + SSE_SERVER_PORT: "7264" + IDENTITY_SERVER_URL: "https://auth.notes.aleshym.co" + NOTESNOOK_APP_HOST: "https://app.notesnook.com" + # S3 (non-secret) + S3_INTERNAL_SERVICE_URL: "http://notesnook-s3:9000" + S3_INTERNAL_BUCKET_NAME: "attachments" + S3_SERVICE_URL: "https://attachments.notes.aleshym.co" + S3_BUCKET_NAME: "attachments" + S3_REGION: "us-east-1" + MINIO_BROWSER: "on" +``` + +- [ ] **Step 4: notesnook-secret.yaml.example** (template; real file applied out-of-band in Task 9) + +```yaml +# DO NOT COMMIT THE REAL VERSION. Copy to notesnook-secret.yaml (gitignored), +# fill real values from the old stack's .env, then: +# kubectl apply -f apps/notesnook/notesnook-secret.yaml +# ArgoCD ignores this Secret's /data (see argocd/notesnook.yaml). +apiVersion: v1 +kind: Secret +metadata: + name: notesnook-secret + namespace: notesnook +stringData: + NOTESNOOK_API_SECRET: "CHANGE_ME" # reuse existing value — do NOT regenerate + SMTP_USERNAME: "funman300@gmail.com" + SMTP_PASSWORD: "CHANGE_ME" # Gmail app password + SMTP_HOST: "smtp.gmail.com" + SMTP_PORT: "587" + MINIO_ROOT_USER: "notesnook" + MINIO_ROOT_PASSWORD: "CHANGE_ME" # reuse existing value + S3_ACCESS_KEY_ID: "notesnook" # = MINIO_ROOT_USER + S3_ACCESS_KEY: "CHANGE_ME" # = MINIO_ROOT_PASSWORD +``` + +- [ ] **Step 5: kustomization.yaml** (all resources; grows as tasks add files) + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: notesnook +resources: + - namespace.yaml + - notesnook-config.yaml + - db-service.yaml + - db-statefulset.yaml + - db-init-job.yaml + - minio-pvc.yaml + - minio-deployment.yaml + - minio-service.yaml + - minio-bucket-job.yaml + - identity-deployment.yaml + - identity-service.yaml + - identity-ingress.yaml + - sync-deployment.yaml + - sync-service.yaml + - sync-ingress.yaml + - sse-deployment.yaml + - sse-service.yaml + - sse-ingress.yaml + - monograph-deployment.yaml + - monograph-service.yaml + - monograph-ingress.yaml + - attachments-ingress.yaml +``` + +- [ ] **Step 6: Apply just the namespace + config, and the real secret, to validate** + +```bash +kubectl apply -f apps/notesnook/namespace.yaml +kubectl apply -f apps/notesnook/notesnook-config.yaml +``` +Expected: `namespace/notesnook created`, `configmap/notesnook-config created`. + +- [ ] **Step 7: Commit** + +```bash +git add apps/notesnook/namespace.yaml apps/notesnook/notesnook-config.yaml apps/notesnook/notesnook-secret.yaml.example apps/notesnook/kustomization.yaml +git commit -m "feat(notesnook): scaffold namespace, config, secret template" +``` + +--- + +### Task 2: MongoDB single-node replica set (`rs0`) + +**Files:** +- Create: `apps/notesnook/db-service.yaml`, `apps/notesnook/db-statefulset.yaml`, `apps/notesnook/db-init-job.yaml` + +**Interfaces:** +- Produces: reachable Mongo at `notesnook-db:27017` with replica set `rs0` initiated (member host `notesnook-db:27017`). Consumed by identity (`/identity`) and sync (`/notesnook`) via `?replSet=rs0`. + +- [ ] **Step 1: db-service.yaml** (headless for stable DNS) + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: notesnook-db + namespace: notesnook +spec: + clusterIP: None + selector: + app: notesnook-db + ports: + - name: mongo + port: 27017 + targetPort: 27017 +``` + +- [ ] **Step 2: db-statefulset.yaml** + +```yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: notesnook-db + namespace: notesnook +spec: + serviceName: notesnook-db + replicas: 1 + selector: + matchLabels: + app: notesnook-db + template: + metadata: + labels: + app: notesnook-db + spec: + containers: + - name: mongod + image: mongo:7.0.12 + args: ["--replSet", "rs0", "--bind_ip_all"] + ports: + - containerPort: 27017 + name: mongo + volumeMounts: + - name: dbdata + mountPath: /data/db + readinessProbe: + exec: + command: ["mongosh", "--quiet", "--eval", "db.runCommand('ping').ok"] + initialDelaySeconds: 10 + periodSeconds: 15 + resources: + requests: { cpu: "100m", memory: "256Mi" } + limits: { cpu: "1000m", memory: "1Gi" } + volumeClaimTemplates: + - metadata: + name: dbdata + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi +``` + +- [ ] **Step 3: db-init-job.yaml** (idempotent replica-set initiation) + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: notesnook-db-init + namespace: notesnook + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 20 + template: + spec: + restartPolicy: OnFailure + containers: + - name: rs-init + image: mongo:7.0.12 + command: ["bash", "-c"] + args: + - | + until mongosh mongodb://notesnook-db:27017 --quiet --eval 'db.runCommand("ping").ok' ; do + echo "waiting for mongod..."; sleep 3; + done + mongosh mongodb://notesnook-db:27017 --quiet --eval ' + try { rs.status() } + catch (e) { rs.initiate({_id:"rs0", members:[{_id:0, host:"notesnook-db:27017"}]}) } + ' + echo "replica set ready" +``` + +- [ ] **Step 4: Apply and verify replica set is PRIMARY** + +```bash +kubectl apply -f apps/notesnook/db-service.yaml -f apps/notesnook/db-statefulset.yaml +kubectl -n notesnook rollout status statefulset/notesnook-db --timeout=180s +kubectl apply -f apps/notesnook/db-init-job.yaml +kubectl -n notesnook wait --for=condition=complete job/notesnook-db-init --timeout=180s +kubectl -n notesnook exec notesnook-db-0 -- mongosh --quiet --eval 'rs.status().members[0].stateStr' +``` +Expected final line: `PRIMARY` + +- [ ] **Step 5: Commit** + +```bash +git add apps/notesnook/db-service.yaml apps/notesnook/db-statefulset.yaml apps/notesnook/db-init-job.yaml +git commit -m "feat(notesnook): mongodb single-node replica set rs0" +``` + +--- + +### Task 3: MinIO (S3) + attachments bucket + +**Files:** +- Create: `apps/notesnook/minio-pvc.yaml`, `apps/notesnook/minio-deployment.yaml`, `apps/notesnook/minio-service.yaml`, `apps/notesnook/minio-bucket-job.yaml` + +**Interfaces:** +- Produces: MinIO at `notesnook-s3:9000` (API) / `:9090` (console), bucket `attachments`. Consumed by sync server (`S3_INTERNAL_SERVICE_URL`) and the `attachments` ingress. +- Consumes: Secret `notesnook-secret` (`MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD`). + +- [ ] **Step 1: minio-pvc.yaml** + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: notesnook-s3-data + namespace: notesnook +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 20Gi +``` + +- [ ] **Step 2: minio-deployment.yaml** + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-s3 + namespace: notesnook +spec: + replicas: 1 + strategy: { type: Recreate } + selector: + matchLabels: + app: notesnook-s3 + template: + metadata: + labels: + app: notesnook-s3 + spec: + containers: + - name: minio + image: minio/minio:RELEASE.2024-07-29T22-14-52Z + args: ["server", "/data/s3", "--console-address", ":9090"] + envFrom: + - secretRef: { name: notesnook-secret } # MINIO_ROOT_USER/PASSWORD + env: + - name: MINIO_BROWSER + value: "on" + ports: + - { containerPort: 9000, name: api } + - { containerPort: 9090, name: console } + volumeMounts: + - name: s3data + mountPath: /data/s3 + readinessProbe: + tcpSocket: { port: 9000 } + initialDelaySeconds: 10 + periodSeconds: 15 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } + volumes: + - name: s3data + persistentVolumeClaim: + claimName: notesnook-s3-data +``` + +- [ ] **Step 3: minio-service.yaml** + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: notesnook-s3 + namespace: notesnook +spec: + selector: + app: notesnook-s3 + ports: + - { name: api, port: 9000, targetPort: 9000 } + - { name: console, port: 9090, targetPort: 9090 } +``` + +- [ ] **Step 4: minio-bucket-job.yaml** (create `attachments`, idempotent) + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: notesnook-s3-setup + namespace: notesnook + annotations: + argocd.argoproj.io/hook: PostSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 20 + template: + spec: + restartPolicy: OnFailure + containers: + - name: mc + image: minio/mc:RELEASE.2024-07-26T13-08-44Z + envFrom: + - secretRef: { name: notesnook-secret } + command: ["/bin/bash", "-c"] + args: + - | + until mc alias set minio http://notesnook-s3:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do + sleep 2; + done + mc mb --ignore-existing minio/attachments + echo "bucket ready" +``` + +- [ ] **Step 5: Apply and verify bucket exists** + +```bash +kubectl apply -f apps/notesnook/minio-pvc.yaml -f apps/notesnook/minio-deployment.yaml -f apps/notesnook/minio-service.yaml +kubectl -n notesnook rollout status deploy/notesnook-s3 --timeout=120s +kubectl apply -f apps/notesnook/minio-bucket-job.yaml +kubectl -n notesnook wait --for=condition=complete job/notesnook-s3-setup --timeout=120s +kubectl -n notesnook logs job/notesnook-s3-setup | tail -1 +``` +Expected: `bucket ready` + +- [ ] **Step 6: Confirm KNOWN_PROXIES CIDR is correct** + +```bash +kubectl -n kube-system get pods -o wide | grep traefik # note pod IP is within 10.42.x.x +``` +Expected: Traefik pod IP inside `10.42.0.0/16`. If not, update `KNOWN_PROXIES` in `notesnook-config.yaml`. + +- [ ] **Step 7: Commit** + +```bash +git add apps/notesnook/minio-*.yaml +git commit -m "feat(notesnook): minio s3 + attachments bucket" +``` + +--- + +### Task 4: Identity/auth server + +**Files:** +- Create: `apps/notesnook/identity-deployment.yaml`, `apps/notesnook/identity-service.yaml`, `apps/notesnook/identity-ingress.yaml` + +**Interfaces:** +- Consumes: ConfigMap `notesnook-config`, Secret `notesnook-secret`, Mongo `notesnook-db` (`/identity`). +- Produces: `notesnook-identity:8264` (health `/health`); ingress `auth.notes.aleshym.co`. + +- [ ] **Step 1: identity-deployment.yaml** + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-identity + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-identity + template: + metadata: + labels: + app: notesnook-identity + spec: + containers: + - name: identity + image: streetwriters/identity:latest + envFrom: + - configMapRef: { name: notesnook-config } + - secretRef: { name: notesnook-secret } + env: + - name: MONGODB_CONNECTION_STRING + value: "mongodb://notesnook-db:27017/identity?replSet=rs0" + - name: MONGODB_DATABASE_NAME + value: "identity" + ports: + - { containerPort: 8264, name: http } + readinessProbe: + httpGet: { path: /health, port: 8264 } + initialDelaySeconds: 20 + periodSeconds: 15 + livenessProbe: + httpGet: { path: /health, port: 8264 } + initialDelaySeconds: 60 + periodSeconds: 30 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } +``` + +- [ ] **Step 2: identity-service.yaml** + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: notesnook-identity + namespace: notesnook +spec: + selector: + app: notesnook-identity + ports: + - { name: http, port: 8264, targetPort: 8264 } +``` + +- [ ] **Step 3: identity-ingress.yaml** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-auth + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: auth.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: notesnook-identity + port: { name: http } + tls: + - hosts: ["auth.notes.aleshym.co"] + secretName: notesnook-auth-tls +``` + +- [ ] **Step 4: Apply and verify pod healthy** (requires the real Secret from Task 9; if not yet applied, apply it now — see Task 9 Step 1) + +```bash +kubectl apply -f apps/notesnook/identity-deployment.yaml -f apps/notesnook/identity-service.yaml -f apps/notesnook/identity-ingress.yaml +kubectl -n notesnook rollout status deploy/notesnook-identity --timeout=180s +kubectl -n notesnook exec deploy/notesnook-identity -- wget -qO- http://localhost:8264/health +``` +Expected: rollout succeeds; health returns a 200 body (non-empty). + +- [ ] **Step 5: Commit** + +```bash +git add apps/notesnook/identity-*.yaml +git commit -m "feat(notesnook): identity/auth server + ingress" +``` + +--- + +### Task 5: Sync server + +**Files:** +- Create: `apps/notesnook/sync-deployment.yaml`, `apps/notesnook/sync-service.yaml`, `apps/notesnook/sync-ingress.yaml` + +**Interfaces:** +- Consumes: ConfigMap, Secret, Mongo (`/notesnook`), MinIO (`notesnook-s3:9000`), identity. +- Produces: `notesnook-sync:5264` (health `/health`); ingress `sync.notes.aleshym.co` + apex `notes.aleshym.co`. + +- [ ] **Step 1: sync-deployment.yaml** + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-sync + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-sync + template: + metadata: + labels: + app: notesnook-sync + spec: + containers: + - name: sync + image: streetwriters/notesnook-sync:latest + envFrom: + - configMapRef: { name: notesnook-config } # incl. S3_* non-secret + - secretRef: { name: notesnook-secret } # incl. S3_ACCESS_KEY* + env: + - name: MONGODB_CONNECTION_STRING + value: "mongodb://notesnook-db:27017/?replSet=rs0" + - name: MONGODB_DATABASE_NAME + value: "notesnook" + ports: + - { containerPort: 5264, name: http } + readinessProbe: + httpGet: { path: /health, port: 5264 } + initialDelaySeconds: 20 + periodSeconds: 15 + livenessProbe: + httpGet: { path: /health, port: 5264 } + initialDelaySeconds: 60 + periodSeconds: 30 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "1000m", memory: "512Mi" } +``` + +- [ ] **Step 2: sync-service.yaml** + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: notesnook-sync + namespace: notesnook +spec: + selector: + app: notesnook-sync + ports: + - { name: http, port: 5264, targetPort: 5264 } +``` + +- [ ] **Step 3: sync-ingress.yaml** (two hosts: sync + apex) + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-sync + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: sync.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-sync, port: { name: http } } + - host: notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-sync, port: { name: http } } + tls: + - hosts: ["sync.notes.aleshym.co", "notes.aleshym.co"] + secretName: notesnook-sync-tls +``` + +- [ ] **Step 4: Apply and verify** + +```bash +kubectl apply -f apps/notesnook/sync-deployment.yaml -f apps/notesnook/sync-service.yaml -f apps/notesnook/sync-ingress.yaml +kubectl -n notesnook rollout status deploy/notesnook-sync --timeout=180s +kubectl -n notesnook exec deploy/notesnook-sync -- wget -qO- http://localhost:5264/health +``` +Expected: rollout succeeds; health returns 200. + +- [ ] **Step 5: Commit** + +```bash +git add apps/notesnook/sync-*.yaml +git commit -m "feat(notesnook): sync server + ingress" +``` + +--- + +### Task 6: SSE/events server + +**Files:** +- Create: `apps/notesnook/sse-deployment.yaml`, `apps/notesnook/sse-service.yaml`, `apps/notesnook/sse-ingress.yaml` + +**Interfaces:** +- Consumes: ConfigMap, Secret, identity + sync (via server-discovery). +- Produces: `notesnook-sse:7264` (health `/health`); ingress `events.notes.aleshym.co`. + +- [ ] **Step 1: sse-deployment.yaml** + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-sse + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-sse + template: + metadata: + labels: + app: notesnook-sse + spec: + containers: + - name: sse + image: streetwriters/sse:latest + envFrom: + - configMapRef: { name: notesnook-config } + - secretRef: { name: notesnook-secret } + ports: + - { containerPort: 7264, name: http } + readinessProbe: + httpGet: { path: /health, port: 7264 } + initialDelaySeconds: 20 + periodSeconds: 15 + livenessProbe: + httpGet: { path: /health, port: 7264 } + initialDelaySeconds: 60 + periodSeconds: 30 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "256Mi" } +``` + +- [ ] **Step 2: sse-service.yaml** + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: notesnook-sse + namespace: notesnook +spec: + selector: + app: notesnook-sse + ports: + - { name: http, port: 7264, targetPort: 7264 } +``` + +- [ ] **Step 3: sse-ingress.yaml** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-sse + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: events.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-sse, port: { name: http } } + tls: + - hosts: ["events.notes.aleshym.co"] + secretName: notesnook-sse-tls +``` + +- [ ] **Step 4: Apply and verify** + +```bash +kubectl apply -f apps/notesnook/sse-deployment.yaml -f apps/notesnook/sse-service.yaml -f apps/notesnook/sse-ingress.yaml +kubectl -n notesnook rollout status deploy/notesnook-sse --timeout=180s +kubectl -n notesnook exec deploy/notesnook-sse -- wget -qO- http://localhost:7264/health +``` +Expected: rollout succeeds; health returns 200. + +- [ ] **Step 5: Commit** + +```bash +git add apps/notesnook/sse-*.yaml +git commit -m "feat(notesnook): sse/events server + ingress" +``` + +--- + +### Task 7: Monograph server + +**Files:** +- Create: `apps/notesnook/monograph-deployment.yaml`, `apps/notesnook/monograph-service.yaml`, `apps/notesnook/monograph-ingress.yaml` + +**Interfaces:** +- Consumes: ConfigMap, Secret, sync (`API_HOST`). +- Produces: `notesnook-monograph:3000`; ingress `monograph.notes.aleshym.co`. + +- [ ] **Step 1: monograph-deployment.yaml** + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notesnook-monograph + namespace: notesnook +spec: + replicas: 1 + selector: + matchLabels: + app: notesnook-monograph + template: + metadata: + labels: + app: notesnook-monograph + spec: + containers: + - name: monograph + image: streetwriters/monograph:latest + envFrom: + - configMapRef: { name: notesnook-config } + - secretRef: { name: notesnook-secret } + env: + - name: HOST + value: "0.0.0.0" + - name: API_HOST + value: "http://notesnook-sync:5264" + - name: PUBLIC_URL + value: "https://monograph.notes.aleshym.co" + ports: + - { containerPort: 3000, name: http } + readinessProbe: + httpGet: { path: /, port: 3000 } + initialDelaySeconds: 20 + periodSeconds: 15 + resources: + requests: { cpu: "50m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } +``` + +- [ ] **Step 2: monograph-service.yaml** + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: notesnook-monograph + namespace: notesnook +spec: + selector: + app: notesnook-monograph + ports: + - { name: http, port: 3000, targetPort: 3000 } +``` + +- [ ] **Step 3: monograph-ingress.yaml** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-monograph + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: monograph.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: notesnook-monograph, port: { name: http } } + tls: + - hosts: ["monograph.notes.aleshym.co"] + secretName: notesnook-monograph-tls +``` + +- [ ] **Step 4: Apply and verify** + +```bash +kubectl apply -f apps/notesnook/monograph-deployment.yaml -f apps/notesnook/monograph-service.yaml -f apps/notesnook/monograph-ingress.yaml +kubectl -n notesnook rollout status deploy/notesnook-monograph --timeout=180s +kubectl -n notesnook exec deploy/notesnook-monograph -- wget -qO- -S http://localhost:3000/ 2>&1 | grep HTTP | head -1 +``` +Expected: rollout succeeds; HTTP status < 500. + +- [ ] **Step 5: Commit** + +```bash +git add apps/notesnook/monograph-*.yaml +git commit -m "feat(notesnook): monograph server + ingress" +``` + +--- + +### Task 8: Attachments ingress (public MinIO) + +**Files:** +- Create: `apps/notesnook/attachments-ingress.yaml` + +**Interfaces:** +- Produces: ingress `attachments.notes.aleshym.co` → `notesnook-s3:9000`. Consumed by clients fetching attachments via S3 presigned URLs. + +- [ ] **Step 1: attachments-ingress.yaml** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notesnook-attachments + namespace: notesnook + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: attachments.notes.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: notesnook-s3 + port: { number: 9000 } + tls: + - hosts: ["attachments.notes.aleshym.co"] + secretName: notesnook-attachments-tls +``` + +- [ ] **Step 2: Apply and verify all ingresses present** + +```bash +kubectl apply -f apps/notesnook/attachments-ingress.yaml +kubectl -n notesnook get ingress +``` +Expected: 5 ingresses (auth, sync, sse, monograph, attachments), all `ADDRESS 10.10.0.100`. + +- [ ] **Step 3: Commit** + +```bash +git add apps/notesnook/attachments-ingress.yaml +git commit -m "feat(notesnook): attachments (minio) public ingress" +``` + +--- + +### Task 9: Apply the real Secret out-of-band + +**Files:** +- Create (gitignored, local only): `apps/notesnook/notesnook-secret.yaml` + +> Do this early enough that Tasks 4–7 pods can start (they mount the Secret). If those tasks were applied before this, restart them afterward: `kubectl -n notesnook rollout restart deploy`. + +- [ ] **Step 1: Build the real secret from the old stack's values** + +```bash +# Pull the three secret values from the old .env (values only; keep them off-screen) +ssh manage@10.10.0.5 'grep -E "^(NOTESNOOK_API_SECRET|SMTP_PASSWORD|MINIO_ROOT_PASSWORD)=" ~/notesnook/.env' +# Copy the template and fill in real values (NOTESNOOK_API_SECRET, SMTP_PASSWORD, +# MINIO_ROOT_PASSWORD, and S3_ACCESS_KEY = MINIO_ROOT_PASSWORD): +cp apps/notesnook/notesnook-secret.yaml.example apps/notesnook/notesnook-secret.yaml +${EDITOR:-nano} apps/notesnook/notesnook-secret.yaml +``` + +- [ ] **Step 2: Apply and confirm it's gitignored** + +```bash +kubectl apply -f apps/notesnook/notesnook-secret.yaml +kubectl -n notesnook get secret notesnook-secret -o jsonpath='{.data.NOTESNOOK_API_SECRET}' | base64 -d | wc -c # >0 +git status --porcelain apps/notesnook/notesnook-secret.yaml # expect NO output (ignored) +``` +Expected: secret exists with non-zero API secret; `git status` shows nothing (file ignored). + +- [ ] **Step 3: Restart any already-running deployments so they pick up the secret** + +```bash +kubectl -n notesnook rollout restart deploy +kubectl -n notesnook rollout status deploy --timeout=180s +``` +Expected: all deployments Ready. + +*(No commit — the real secret is never committed.)* + +--- + +### Task 10: Migrate data (Mongo + MinIO) — dry run before cutover + +**Goal:** Copy `identity` + `notesnook` DBs and the `attachments` bucket from the old stack into k8s. Run once as a rehearsal now (safe — additive), and again as the final sync during cutover (Task 11). + +**Interfaces:** +- Consumes: old stack Mongo (`notesnook-db` container on `10.10.0.5`) and MinIO; k8s Mongo (`notesnook-db-0`) and MinIO. + +- [ ] **Step 1: Dump + restore MongoDB (identity + notesnook)** + +```bash +# Dump from old compose Mongo to a local archive +ssh manage@10.10.0.5 'docker exec notesnook-db mongodump --archive --db=identity' > /tmp/identity.archive +ssh manage@10.10.0.5 'docker exec notesnook-db mongodump --archive --db=notesnook' > /tmp/notesnook.archive +# Restore into k8s Mongo +kubectl -n notesnook exec -i notesnook-db-0 -- mongorestore --archive --drop < /tmp/identity.archive +kubectl -n notesnook exec -i notesnook-db-0 -- mongorestore --archive --drop < /tmp/notesnook.archive +``` + +- [ ] **Step 2: Verify collection counts match** + +```bash +echo "OLD identity users:"; ssh manage@10.10.0.5 'docker exec notesnook-db mongosh identity --quiet --eval "db.getCollectionNames().length"' +echo "NEW identity colls:"; kubectl -n notesnook exec notesnook-db-0 -- mongosh identity --quiet --eval "db.getCollectionNames().length" +echo "OLD notesnook colls:"; ssh manage@10.10.0.5 'docker exec notesnook-db mongosh notesnook --quiet --eval "db.getCollectionNames().length"' +echo "NEW notesnook colls:"; kubectl -n notesnook exec notesnook-db-0 -- mongosh notesnook --quiet --eval "db.getCollectionNames().length" +``` +Expected: OLD == NEW for both DBs. + +- [ ] **Step 3: Mirror the attachments bucket** + +```bash +# Port-forward the k8s MinIO API locally, then mirror old -> new with mc. +kubectl -n notesnook port-forward svc/notesnook-s3 9000:9000 & # PF_PID=$! +PF_PID=$!; sleep 3 +# Old minio creds = MINIO_ROOT_USER/PASSWORD (same on both sides after Task 9) +mc alias set oldminio "http://10.10.0.5:9000" "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" 2>/dev/null || \ + ssh -f -N -L 9001:notesnook-s3:9000 manage@10.10.0.5 # if old minio not directly reachable, tunnel to 9001 and use that +mc alias set newminio "http://127.0.0.1:9000" "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" +mc mirror --overwrite oldminio/attachments newminio/attachments +kill $PF_PID +``` +Note: the old MinIO API isn't publicly exposed (only `attachments.*` via presigned GETs); reach it via the compose network from `10.10.0.5` or an SSH tunnel as shown. + +- [ ] **Step 4: Verify object parity** + +```bash +mc du oldminio/attachments ; mc du newminio/attachments +``` +Expected: matching object count / size. + +*(No commit — data operation.)* + +--- + +### Task 11: Register with ArgoCD, merge, and cut over + +**Files:** +- Create: `argocd/notesnook.yaml` + +**Interfaces:** +- Produces: ArgoCD `Application` adopting `apps/notesnook`; DNS repoint on `10.10.0.8`. + +- [ ] **Step 1: argocd/notesnook.yaml** + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: notesnook + namespace: argocd +spec: + project: default + source: + repoURL: https://git.aleshym.co/funman300/k3s-homelab.git + targetRevision: main + path: apps/notesnook + destination: + server: https://kubernetes.default.svc + namespace: notesnook + ignoreDifferences: + - group: "" + kind: Secret + name: notesnook-secret + namespace: notesnook + jsonPointers: + - /data + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true +``` + +- [ ] **Step 2: Validate the full kustomize build (server dry-run)** + +```bash +kubectl apply -k apps/notesnook --dry-run=server +kustomize build apps/notesnook >/dev/null && echo "kustomize OK" +``` +Expected: no errors; `kustomize OK`. + +- [ ] **Step 3: Pre-cutover TLS + endpoint check via /etc/hosts override** + +```bash +# Temporarily map the hosts to 10.10.0.100 locally to test before flipping real DNS +for h in auth sync events monograph attachments; do echo "10.10.0.100 $h.notes.aleshym.co"; done | sudo tee -a /etc/hosts +curl -sS -o /dev/null -w "auth %{http_code}\n" https://auth.notes.aleshym.co/health +curl -sS -o /dev/null -w "sync %{http_code}\n" https://sync.notes.aleshym.co/health +# (cert-manager issues certs via Cloudflare DNS-01 regardless of A records) +kubectl -n notesnook get certificate +``` +Expected: HTTP 200; all `Certificate` resources `READY=True`. Remove the temporary `/etc/hosts` lines afterward. + +- [ ] **Step 4: Push branch, open PR, merge to main (ArgoCD adopts running resources)** + +```bash +git push -u origin feat/notesnook-k8s +# open PR in Gitea, review, merge to main +# ArgoCD syncs apps/notesnook; since resources already exist identically, expect Synced/Healthy with no churn +kubectl -n argocd wait --for=jsonpath='{.status.sync.status}'=Synced application/notesnook --timeout=180s +``` +Expected: Application `Synced` + `Healthy`. + +- [ ] **Step 5: Freeze writes on old stack + final data sync** + +```bash +# Stop only the app services on the old stack (keep db + minio for the final dump) +ssh manage@10.10.0.5 'cd ~/notesnook && docker compose stop identity-server notesnook-server sse-server monograph-server' +# Re-run Task 10 Steps 1 & 3 (mongorestore --drop + mc mirror --overwrite) for the final delta +``` +Expected: final restore/mirror complete with matching counts. + +- [ ] **Step 6: DNS cutover on 10.10.0.8** + +``` +# On the internal DNS server (10.10.0.8 — AdGuard/Pi-hole/unbound), repoint these +# A records from 10.10.0.5 to 10.10.0.100: +# auth.notes.aleshym.co, sync.notes.aleshym.co, events.notes.aleshym.co, +# monograph.notes.aleshym.co, attachments.notes.aleshym.co, notes.aleshym.co +# Then flush caches. +``` +Verify from a client: +```bash +for h in auth sync events monograph attachments; do printf "%-12s " "$h"; getent hosts $h.notes.aleshym.co | awk '{print $1}'; done +``` +Expected: all → `10.10.0.100`. + +- [ ] **Step 7: End-to-end validation** + +- Request a login code in the Notesnook client → email arrives (Gmail SMTP). +- Log into the **existing** account (no re-signup). +- Confirm pre-existing notes appear after sync. +- Open a note with an attachment → downloads via `attachments.notes.aleshym.co`. +- Open a monograph public link → renders. + +- [ ] **Step 8: Stop the old stack (keep data for rollback)** + +```bash +ssh manage@10.10.0.5 'cd ~/notesnook && docker compose down' # NO -v; data retained ~1 week +``` +Expected: containers removed, `dbdata`/`s3data` volumes retained. + +--- + +### Task 12: Decommission (≈1 week after stable cutover) + +- [ ] **Step 1: Confirm k8s stack stable for ~1 week** (no sync errors, backups if any in place). + +- [ ] **Step 2: Remove old stack + data** + +```bash +ssh manage@10.10.0.5 'cd ~/notesnook && docker compose down -v && cd ~ && rm -rf ~/notesnook' +``` +Expected: volumes and compose dir removed. + +- [ ] **Step 3: Update memory** — edit `notesnook-selfhost-smtp` memory to note the app now runs in k8s (`apps/notesnook`, ns `notesnook`), SMTP config lives in the `notesnook-secret` Secret, and DNS points at `10.10.0.100`. + +--- + +## Self-Review notes + +- **Spec coverage:** every spec component (Mongo rs0, MinIO+bucket, 4 services, 5 ingresses, ConfigMap/Secret split, data migration, DNS cutover on 10.10.0.8, keep-then-remove old stack, monograph included) maps to Tasks 1–12. ✓ +- **Secret handling:** `.example` committed, real gitignored, ArgoCD `ignoreDifferences` on Secret /data (Task 11). ✓ +- **Type/name consistency:** service names (`notesnook-db/-s3/-identity/-sync/-sse/-monograph`), ConfigMap `notesnook-config`, Secret `notesnook-secret`, TLS secrets `notesnook--tls` used consistently across tasks. ✓ +- **Ordering caveat:** the real Secret (Task 9) must exist before identity/sync/sse/monograph pods become Ready; execute Task 9 right after Task 3 if running strictly in order, or `rollout restart` after. +``` diff --git a/docs/superpowers/specs/2026-07-15-notesnook-k8s-migration-design.md b/docs/superpowers/specs/2026-07-15-notesnook-k8s-migration-design.md new file mode 100644 index 0000000..8a03493 --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-notesnook-k8s-migration-design.md @@ -0,0 +1,222 @@ +# Notesnook self-host → k3s migration — design + +**Date:** 2026-07-15 +**Status:** Approved design, pending spec review → implementation plan + +## Goal + +Move the self-hosted Notesnook stack from the docker-compose deployment on host +`10.10.0.5` (LXC on Proxmox `pve`) into the k3s cluster (CT 114, node IP +`10.10.0.100`), managed by ArgoCD via the `k3s-homelab` GitOps repo. The existing +account, notes, encryption keys, and attachments must be preserved. + +## Current state (source) + +- **Host:** `manage@10.10.0.5`, docker-compose stack in `~/notesnook`. +- **Services:** MongoDB 7.0.12 (replica set `rs0`), MinIO (S3), identity/auth, + sync, sse/events, monograph, plus bootstrap helpers (`validate`, `setup-s3`) + and `autoheal`. +- **Public hosts** (Traefik on the compose box, cert via Cloudflare resolver): + `auth`, `sync` (+ apex `notes`), `events`, `monograph`, `attachments`, + `minio` — all under `notes.aleshym.co`. +- **Data lives in:** Mongo databases `identity` and `notesnook`; MinIO bucket + `attachments`. +- **Root cause history:** login-code email was failing because SMTP was left at + `CHANGE_ME`; now fixed to Gmail SMTP. That corrected config carries into the + migration. + +## Target cluster conventions (observed) + +- **GitOps:** ArgoCD (no app-of-apps). Monorepo + `https://git.aleshym.co/funman300/k3s-homelab.git`, branch `main`. App + manifests under `apps//` (kustomize); the `Application` CR committed at + `argocd/.yaml` with `automated {prune, selfHeal}` + `CreateNamespace=true`. +- **Ingress:** Traefik (`ingressClassName: traefik`), entrypoint `websecure`. +- **TLS:** cert-manager clusterissuer `letsencrypt-prod` via **Cloudflare DNS-01**. + Per-host TLS secret named `-tls`. +- **Storage:** single-node `local-path` (default), `ReadWriteOnce`, `Delete` + reclaim. **Implication:** deleting a PVC deletes its data — treat Mongo/MinIO + PVCs as precious. +- **Secrets:** no sealed-secrets / external-secrets controller. Convention is a + committed `*-secret.yaml.example`; the real `*-secret.yaml` is **gitignored** + (`apps/**/*-secret.yaml`) and applied once with `kubectl apply`. The + `Application` CR uses `ignoreDifferences` on the Secret's `/data` so ArgoCD + won't fight the out-of-band secret. +- **Internal DNS:** resolver `10.10.0.8` holds split-horizon `*.aleshym.co` → LAN + IP records (`ns.aleshym.co → 10.10.0.100`, `auth.notes.aleshym.co → 10.10.0.5` + today). This is where the cutover repoint happens. Public authority for + `aleshym.co` is Cloudflare (used only for the ACME DNS-01 challenge). + +## Architecture + +Namespace: `notesnook`. Repo path: `apps/notesnook/`. ArgoCD app: +`argocd/notesnook.yaml`. + +| Component | Image | k8s workload | Service (in-cluster) | Public ingress host → port | +|---|---|---|---|---| +| MongoDB `rs0` | `mongo:7.0.12` | StatefulSet (1) + headless Service + init Job | `notesnook-db:27017` | none | +| MinIO | `minio/minio:RELEASE.2024-07-29T22-14-52Z` | Deployment + PVC | `notesnook-s3:9000` | `attachments.notes.aleshym.co` → 9000 | +| Identity/auth | `streetwriters/identity:latest` | Deployment | `notesnook-identity:8264` | `auth.notes.aleshym.co` → 8264 | +| Sync | `streetwriters/notesnook-sync:latest` | Deployment | `notesnook-sync:5264` | `sync.notes.aleshym.co` + `notes.aleshym.co` → 5264 | +| SSE/events | `streetwriters/sse:latest` | Deployment | `notesnook-sse:7264` | `events.notes.aleshym.co` → 7264 | +| Monograph | `streetwriters/monograph:latest` | Deployment | `notesnook-monograph:3000` | `monograph.notes.aleshym.co` → 3000 | + +**Dropped from compose:** `autoheal` (replaced by k8s liveness/readiness probes), +`validate` (env presence enforced by manifests), `setup-s3` (folded into a MinIO +bucket-create init Job / or `mc` init container). + +### MongoDB replica set (the one real deviation from the nightscout template) + +Notesnook's connection strings use `?replSet=rs0` and the sync server relies on +change streams/transactions, which require a replica set. Plan: + +- **StatefulSet** (1 replica) `notesnook-db`, `command: [mongod, --replSet, rs0, --bind_ip_all]`. +- **Headless Service** `notesnook-db` for stable DNS. +- **One-time init Job** (or `postStart`) runs + `rs.initiate({_id:"rs0", members:[{_id:0, host:"notesnook-db:27017"}]})`, + idempotent (skip if already initialized). The RS member host must be the stable + Service name `notesnook-db:27017` so clients using `?replSet=rs0` resolve the + primary correctly. +- No Mongo auth (matches current compose: `mongodb://notesnook-db:27017`, no + credentials). Access is namespace-internal only. + +### Config vs secrets + +**ConfigMap `notesnook-config`** (committed, non-secret): + +``` +INSTANCE_NAME=aleshym-notesnook +DISABLE_SIGNUPS=true +NOTESNOOK_APP_PUBLIC_URL=https://app.notesnook.com +AUTH_SERVER_PUBLIC_URL=https://auth.notes.aleshym.co +MONOGRAPH_PUBLIC_URL=https://monograph.notes.aleshym.co +ATTACHMENTS_SERVER_PUBLIC_URL=https://attachments.notes.aleshym.co +NOTESNOOK_CORS_ORIGINS=https://app.notesnook.com,https://notes.aleshym.co +KNOWN_PROXIES= # may need the Traefik pod CIDR; see open items +# service-discovery (in-cluster names replace compose hostnames) +NOTESNOOK_SERVER_HOST=notesnook-sync +NOTESNOOK_SERVER_PORT=5264 +IDENTITY_SERVER_HOST=notesnook-identity +IDENTITY_SERVER_PORT=8264 +SSE_SERVER_HOST=notesnook-sse +SSE_SERVER_PORT=7264 +IDENTITY_SERVER_URL=https://auth.notes.aleshym.co +NOTESNOOK_APP_HOST=https://app.notesnook.com +# S3/MinIO non-secret +S3_INTERNAL_SERVICE_URL=http://notesnook-s3:9000 +S3_INTERNAL_BUCKET_NAME=attachments +S3_SERVICE_URL=https://attachments.notes.aleshym.co +S3_BUCKET_NAME=attachments +S3_REGION=us-east-1 +MONGODB_CONNECTION_STRING (identity)=mongodb://notesnook-db:27017/identity?replSet=rs0 +MONGODB_CONNECTION_STRING (sync)=mongodb://notesnook-db:27017/?replSet=rs0 +``` + +**Secret `notesnook-secret`** (out-of-band, `*.example` committed, real gitignored): + +``` +NOTESNOOK_API_SECRET=... # reuse existing value from old .env +SMTP_USERNAME=funman300@gmail.com +SMTP_PASSWORD=... # Gmail app password +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +MINIO_ROOT_USER=notesnook +MINIO_ROOT_PASSWORD=... # reuse existing value +S3_ACCESS_KEY_ID=notesnook # = MINIO_ROOT_USER +S3_ACCESS_KEY=... # = MINIO_ROOT_PASSWORD +# TWILIO_* : present in old .env but SMS 2FA unused; omit unless in use +``` + +Reusing `NOTESNOOK_API_SECRET`, MinIO creds, and the Mongo data verbatim is what +keeps the migrated account valid — do **not** regenerate the API secret. + +## Repo layout to add + +``` +apps/notesnook/ + kustomization.yaml + namespace.yaml + notesnook-config.yaml # ConfigMap + notesnook-secret.yaml.example # secret template (real applied out-of-band) + db-statefulset.yaml # mongo rs0 + db-service.yaml # headless + db-init-job.yaml # rs.initiate (idempotent) + minio-pvc.yaml + minio-deployment.yaml + minio-service.yaml + minio-bucket-job.yaml # create 'attachments' bucket + identity-deployment.yaml + identity-service.yaml + identity-ingress.yaml + sync-deployment.yaml + sync-service.yaml + sync-ingress.yaml + sse-deployment.yaml + sse-service.yaml + sse-ingress.yaml + monograph-deployment.yaml+ monograph-service.yaml+ monograph-ingress.yaml + attachments-ingress.yaml # attachments.notes.aleshym.co -> notesnook-s3:9000 +argocd/notesnook.yaml +``` + +`.gitignore` already covers `apps/**/*-secret.yaml`. + +## Data migration procedure + +Runs during the cutover window; both stacks reachable from the migration host. + +1. **Mongo:** `mongodump` the `identity` and `notesnook` DBs from the compose + Mongo (`docker exec notesnook-db mongodump --archive`), stream to the k8s Mongo + via `kubectl exec` + `mongorestore --archive --nsInclude 'identity.*' + --nsInclude 'notesnook.*'`. Verify collection counts match on both sides. +2. **MinIO:** `mc mirror` the `attachments` bucket from the compose MinIO to the + k8s MinIO (mirror via a temporary `mc` pod/job with both aliases, or + port-forward). Verify object count/size. +3. **Sanity:** confirm identity DB user document and notesnook sync docs present. + +## Cutover procedure + +1. Deploy the empty k8s stack (commit → ArgoCD sync). Verify all pods healthy, + ingress created, **TLS certs issued** for all 5 hosts (temporary test hostnames + or `/etc/hosts` override so we can validate before flipping real DNS). +2. Announce brief downtime. Stop client sync / freeze writes on old stack + (`docker compose stop` the app services, keep DB+MinIO up for the dump). +3. Run the data migration (above). +4. **DNS cutover on `10.10.0.8`:** repoint + `auth`/`sync`/`events`/`monograph`/`attachments`.notes.aleshym.co **and** apex + `notes.aleshym.co` from `10.10.0.5` → `10.10.0.100`. Lower TTL beforehand if + possible; flush caches after. +5. Verify end-to-end against k8s: request a login code (email arrives), log in, + sync a note, open an attachment, load a monograph link. +6. `docker compose down` the old stack (data retained) — rollback = flip DNS back + and `docker compose up -d`. + +## Rollback + +Until decommission, rollback is: repoint the 6 records on `10.10.0.8` back to +`10.10.0.5` and `docker compose up -d` on the old host. Because writes were frozen +during cutover, no data diverges. + +## Decommission (≈1 week after stable cutover) + +Remove the compose stack and its data on `10.10.0.5` +(`docker compose down -v`, remove `~/notesnook`), and reclaim the LXC if no longer +needed. Update memory notes to point at the k8s deployment. + +## Verification / success criteria + +- All Notesnook pods `Running`/healthy; Mongo RS `PRIMARY`; MinIO bucket present. +- All 5 ingress hosts serve valid Let's Encrypt certs. +- Existing account logs in (email code delivered) — **no re-signup**. +- Pre-existing notes + attachments visible in the client after sync. +- Monograph public link resolves. + +## Open items / access still needed + +- **Git push access** to `git.aleshym.co/funman300/k3s-homelab` (clone was + anonymous/read-only). Decide branch vs PR workflow. +- **Edit access to internal DNS `10.10.0.8`** for the cutover (mechanism/creds TBD + — AdGuard/Pi-hole/unbound?). +- **`KNOWN_PROXIES`:** old value empty; behind Traefik the sync/identity servers + may need the Traefik pod source CIDR here for correct client-IP/proxy handling. + Confirm during implementation. +- **Public access path:** are the `notes.*` hosts reachable externally (Cloudflare + tunnel / netbird), or LAN/VPN-only? Affects whether any Cloudflare records also + need updating, or just internal DNS. +- **Twilio SMS 2FA:** present in old `.env`; assumed unused (email 2FA works). Omit + unless confirmed in use. +```