Adds apps/notesnook (namespace, config, mongo rs0 StatefulSet, minio, identity/sync/sse/monograph deployments + Traefik/cert-manager ingresses) and argocd/notesnook.yaml. Secret applied out-of-band (only .example committed). Includes design spec + implementation plan under docs/superpowers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
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) andautoheal. - Public hosts (Traefik on the compose box, cert via Cloudflare resolver):
auth,sync(+ apexnotes),events,monograph,attachments,minio— all undernotes.aleshym.co. - Data lives in: Mongo databases
identityandnotesnook; MinIO bucketattachments. - 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, branchmain. App manifests underapps/<name>/(kustomize); theApplicationCR committed atargocd/<name>.yamlwithautomated {prune, selfHeal}+CreateNamespace=true. - Ingress: Traefik (
ingressClassName: traefik), entrypointwebsecure. - TLS: cert-manager clusterissuer
letsencrypt-prodvia Cloudflare DNS-01. Per-host TLS secret named<something>-tls. - Storage: single-node
local-path(default),ReadWriteOnce,Deletereclaim. 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.yamlis gitignored (apps/**/*-secret.yaml) and applied once withkubectl apply. TheApplicationCR usesignoreDifferenceson the Secret's/dataso ArgoCD won't fight the out-of-band secret. - Internal DNS: resolver
10.10.0.8holds split-horizon*.aleshym.co→ LAN IP records (ns.aleshym.co → 10.10.0.100,auth.notes.aleshym.co → 10.10.0.5today). This is where the cutover repoint happens. Public authority foraleshym.cois 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-dbfor stable DNS. - One-time init Job (or
postStart) runsrs.initiate({_id:"rs0", members:[{_id:0, host:"notesnook-db:27017"}]}), idempotent (skip if already initialized). The RS member host must be the stable Service namenotesnook-db:27017so clients using?replSet=rs0resolve 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.
- Mongo:
mongodumptheidentityandnotesnookDBs from the compose Mongo (docker exec notesnook-db mongodump --archive), stream to the k8s Mongo viakubectl exec+mongorestore --archive --nsInclude 'identity.*' --nsInclude 'notesnook.*'. Verify collection counts match on both sides. - MinIO:
mc mirrortheattachmentsbucket from the compose MinIO to the k8s MinIO (mirror via a temporarymcpod/job with both aliases, or port-forward). Verify object count/size. - Sanity: confirm identity DB user document and notesnook sync docs present.
Cutover procedure
- 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/hostsoverride so we can validate before flipping real DNS). - Announce brief downtime. Stop client sync / freeze writes on old stack
(
docker compose stopthe app services, keep DB+MinIO up for the dump). - Run the data migration (above).
- DNS cutover on
10.10.0.8: repointauth/sync/events/monograph/attachments.notes.aleshym.co and apexnotes.aleshym.cofrom10.10.0.5→10.10.0.100. Lower TTL beforehand if possible; flush caches after. - Verify end-to-end against k8s: request a login code (email arrives), log in, sync a note, open an attachment, load a monograph link.
docker compose downthe old stack (data retained) — rollback = flip DNS back anddocker 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 RSPRIMARY; 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.8for 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.