4b801e3444
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>
1193 lines
38 KiB
Markdown
1193 lines
38 KiB
Markdown
# 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 <repo>
|
||
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-<host>-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.
|
||
```
|