Initial commit: Complete NodeJS-native setup

- Migrated from Python pre-commit to NodeJS-native solution
- Reorganized documentation structure
- Set up Husky + lint-staged for efficient pre-commit hooks
- Fixed Dockerfile healthcheck issue
- Added comprehensive documentation index
This commit is contained in:
William Valentin
2025-09-06 01:42:48 -07:00
commit e48adbcb00
159 changed files with 24405 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
# Kubernetes Manifests for RxMinder
This directory contains Kubernetes manifests and templates for deploying RxMinder on a Kubernetes cluster.
## 🎯 Template-Based Deployment (Recommended)
RxMinder uses **template files** with environment variable substitution for secure, user-friendly deployment.
### **Template Files**
- `couchdb-secret.yaml.template` - Database credentials (uses `stringData` - no base64 encoding needed!)
- `ingress.yaml.template` - Ingress configuration with customizable hostname
- `configmap.yaml.template` - Application configuration
- `frontend-deployment.yaml.template` - Frontend deployment
### **Static Files**
- `couchdb-statefulset.yaml` - StatefulSet for CouchDB database
- `couchdb-service.yaml` - Service to expose CouchDB
- `couchdb-pvc.yaml` - PersistentVolumeClaim for CouchDB storage
- `db-seed-job.yaml` - Job to seed initial database data
- `frontend-service.yaml` - Service to expose frontend
- `hpa.yaml` - Horizontal Pod Autoscaler
- `network-policy.yaml` - Network security policies
## 🚀 Deployment Instructions
### **Option 1: Template-Based Deployment (Recommended)**
```bash
# 1. Copy and configure environment
cp .env.example .env
# 2. Edit .env with your settings
nano .env
# Set: APP_NAME, COUCHDB_PASSWORD, INGRESS_HOST, etc.
# 3. Deploy with templates
./scripts/k8s-deploy-template.sh deploy
# 4. Check status
./scripts/k8s-deploy-template.sh status
# 5. Cleanup (if needed)
./scripts/k8s-deploy-template.sh delete
```
**Benefits of template approach:**
- ✅ No manual base64 encoding required
- ✅ Secure credential management via `.env`
- ✅ Automatic dependency ordering
- ✅ Built-in validation and status checking
- ✅ Easy customization of app name and configuration
### **Option 2: Manual Deployment**
For advanced users who want manual control:
```bash
# Manual template processing (requires envsubst)
envsubst < couchdb-secret.yaml.template > /tmp/couchdb-secret.yaml
envsubst < ingress.yaml.template > /tmp/ingress.yaml
# Apply resources in order
kubectl apply -f /tmp/couchdb-secret.yaml
kubectl apply -f couchdb-pvc.yaml
kubectl apply -f couchdb-service.yaml
kubectl apply -f couchdb-statefulset.yaml
kubectl apply -f configmap.yaml.template
kubectl apply -f frontend-deployment.yaml.template
kubectl apply -f frontend-service.yaml
kubectl apply -f /tmp/ingress.yaml
kubectl apply -f network-policy.yaml
kubectl apply -f hpa.yaml
kubectl apply -f db-seed-job.yaml
```
### **Environment Configuration**
Create `.env` with these required variables:
```bash
# Application Configuration
APP_NAME=rxminder # Customize your app name
INGRESS_HOST=rxminder.yourdomain.com # Your external hostname
# Docker Image Configuration
DOCKER_IMAGE=myregistry.com/rxminder:v1.0.0 # Your container image
# Database Credentials
COUCHDB_USER=admin
COUCHDB_PASSWORD=super-secure-password-123
# Storage Configuration
STORAGE_CLASS=longhorn # Your cluster's storage class
STORAGE_SIZE=20Gi # Database storage allocation
# Optional: Advanced Configuration
VITE_COUCHDB_URL=http://localhost:5984
APP_BASE_URL=https://rxminder.yourdomain.com
```
### **Docker Image Options**
Configure the container image based on your registry:
| Registry Type | Example Image | Use Case |
| ----------------------------- | -------------------------------------------------------------- | ------------------ |
| **Docker Hub** | `rxminder/rxminder:v1.0.0` | Public releases |
| **GitHub Container Registry** | `ghcr.io/username/rxminder:latest` | GitHub integration |
| **AWS ECR** | `123456789012.dkr.ecr.us-west-2.amazonaws.com/rxminder:v1.0.0` | AWS deployments |
| **Google GCR** | `gcr.io/project-id/rxminder:stable` | Google Cloud |
| **Private Registry** | `registry.company.com/rxminder:production` | Enterprise |
| **Local Registry** | `localhost:5000/rxminder:dev` | Development |
### **Storage Class Options**
Choose the appropriate storage class for your environment:
| Platform | Recommended Storage Class | Notes |
| --------------------------- | ------------------------- | -------------------------------- |
| **Raspberry Pi + Longhorn** | `longhorn` | Distributed storage across nodes |
| **k3s** | `local-path` | Single-node local storage |
| **AWS EKS** | `gp3` or `gp2` | General Purpose SSD |
| **Google GKE** | `pd-ssd` | SSD Persistent Disk |
| **Azure AKS** | `managed-premium` | Premium SSD |
**Check available storage classes:**
```bash
kubectl get storageclass
```
```bash
# Kubernetes Ingress Configuration
INGRESS_HOST=app.meds.192.168.1.100.nip.io # Your cluster IP
# For production with custom domain
INGRESS_HOST=meds.yourdomain.com
```
## Credentials
The CouchDB credentials are stored in a Kubernetes secret. **IMPORTANT**: Update the credentials in `couchdb-secret.yaml` with your own secure values before deploying to production.
## Architecture
```
┌─────────────────────────────────────┐
│ Frontend Pod │
│ ┌─────────────────────────────────┐│
│ │ React Application ││
│ │ • Authentication Service ││ ← Embedded in frontend
│ │ • UI Components ││
│ │ • Medication Management ││
│ │ • Email Integration ││
│ └─────────────────────────────────┘│
└─────────────────────────────────────┘
↓ HTTP API
┌─────────────────────────────────────┐
│ CouchDB StatefulSet │
│ • User Data & Authentication │
│ • Medication Records │
│ • Persistent Storage │
└─────────────────────────────────────┘
```
**Key Features:**
- **Monolithic Frontend**: Single container with all functionality
- **Database**: CouchDB running as a StatefulSet with persistent storage
- **Storage**: Longhorn for persistent volume management
- **Networking**: Services configured for proper communication between components
## Raspberry Pi Compatibility
All manifests use multi-architecture images and are optimized for ARM architecture commonly used in Raspberry Pi clusters.
## Important Notes
- The PVC uses Longhorn storage class for persistent storage
- CouchDB runs as a StatefulSet for stable network identifiers
- Frontend is exposed via LoadBalancer service
- CouchDB is exposed via ClusterIP service (internal access only)
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ${APP_NAME:-rxminder}-config
labels:
app: ${APP_NAME:-rxminder}
data:
NODE_ENV: 'production'
REACT_APP_API_URL: 'http://couchdb-service:5984'
LOG_LEVEL: 'info'
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ${APP_NAME}-config
labels:
app: ${APP_NAME}
data:
NODE_ENV: "production"
REACT_APP_API_URL: "http://${APP_NAME}-couchdb-service:5984"
LOG_LEVEL: "info"
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${APP_NAME}-couchdb-pvc
labels:
app: ${APP_NAME}
component: database
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${APP_NAME}-couchdb-pvc
labels:
app: ${APP_NAME}
component: database
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: couchdb-secret
labels:
app: ${APP_NAME:-rxminder}
component: database
type: Opaque
stringData:
# These values will be automatically base64 encoded by Kubernetes
# Update these in your .env file before deployment
username: ${COUCHDB_USER:-admin}
password: ${COUCHDB_PASSWORD:-change-this-secure-password}
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: couchdb-secret
labels:
app: ${APP_NAME}
component: database
type: Opaque
stringData:
# These values will be automatically base64 encoded by Kubernetes
# Update these in your .env file before deployment
username: ${COUCHDB_USER}
password: ${COUCHDB_PASSWORD}
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}-couchdb-service
labels:
app: ${APP_NAME}
component: database
spec:
selector:
app: ${APP_NAME}
component: database
ports:
- name: couchdb
port: 5984
targetPort: 5984
protocol: TCP
type: ClusterIP
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}-couchdb-service
labels:
app: ${APP_NAME}
component: database
spec:
selector:
app: ${APP_NAME}
component: database
ports:
- name: couchdb
port: 5984
targetPort: 5984
protocol: TCP
type: ClusterIP
+70
View File
@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ${APP_NAME}-couchdb
labels:
app: ${APP_NAME}
component: database
spec:
serviceName: ${APP_NAME}-couchdb-service
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
component: database
template:
metadata:
labels:
app: ${APP_NAME}
component: database
spec:
containers:
- name: couchdb
image: couchdb:3.3.2
ports:
- containerPort: 5984
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-secret
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-secret
key: password
resources:
requests:
memory: '64Mi'
cpu: '30m'
limits:
memory: '128Mi'
cpu: '60m'
volumeMounts:
- name: couchdb-data
mountPath: /opt/couchdb/data
livenessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: couchdb-data
labels:
app: ${APP_NAME}
component: database
spec:
accessModes: ['ReadWriteOnce']
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+70
View File
@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ${APP_NAME}-couchdb
labels:
app: ${APP_NAME}
component: database
spec:
serviceName: ${APP_NAME}-couchdb-service
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
component: database
template:
metadata:
labels:
app: ${APP_NAME}
component: database
spec:
containers:
- name: couchdb
image: couchdb:3.3.2
ports:
- containerPort: 5984
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-secret
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-secret
key: password
resources:
requests:
memory: "64Mi"
cpu: "30m"
limits:
memory: "128Mi"
cpu: "60m"
volumeMounts:
- name: couchdb-data
mountPath: /opt/couchdb/data
livenessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /_up
port: 5984
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: couchdb-data
labels:
app: ${APP_NAME}
component: database
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ${STORAGE_CLASS}
resources:
requests:
storage: ${STORAGE_SIZE}
+107
View File
@@ -0,0 +1,107 @@
apiVersion: batch/v1
kind: Job
metadata:
name: db-seed
labels:
app: rxminder
component: database
spec:
template:
metadata:
labels:
app: rxminder
component: database
spec:
containers:
- name: db-seeder
image: couchdb:3.3.2
env:
- name: COUCHDB_USER
valueFrom:
secretKeyRef:
name: couchdb-secret
key: username
- name: COUCHDB_PASSWORD
valueFrom:
secretKeyRef:
name: couchdb-secret
key: password
command: ['/bin/sh', '-c']
args:
- |
# Wait for CouchDB to be ready
echo "Waiting for CouchDB to be ready..."
until curl -f http://couchdb-service:5984/_up 2>/dev/null; do
sleep 2
done
# Create databases
echo "Creating databases..."
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app
# Create default admin user
echo "Creating default admin user..."
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/_users/org.couchdb.user:$COUCHDB_USER \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$COUCHDB_USER\",
\"password\": \"$COUCHDB_PASSWORD\",
\"roles\": [\"admin\"],
\"type\": \"user\"
}"
# Create design documents for views
echo "Creating design documents..."
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/medications \
-H "Content-Type: application/json" \
-d '{
"views": {
"by_name": {
"map": "function(doc) { if (doc.type === \"medication\") emit(doc.name, doc); }"
},
"by_user": {
"map": "function(doc) { if (doc.type === \"medication\") emit(doc.userId, doc); }"
}
}
}'
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/reminders \
-H "Content-Type: application/json" \
-d '{
"views": {
"by_medication": {
"map": "function(doc) { if (doc.type === \"reminder\") emit(doc.medicationId, doc); }"
},
"by_user": {
"map": "function(doc) { if (doc.type === \"reminder\") emit(doc.userId, doc); }"
}
}
}'
# Create a sample user document for reference
# Create design document for authentication users
curl -X PUT http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app/_design/auth \
-H "Content-Type: application/json" \
-d '{
"views": {
"by_username": {
"map": "function(doc) { if (doc.type === \"user\" && doc.username) emit(doc.username, doc); }"
},
"by_email": {
"map": "function(doc) { if (doc.type === \"user\" && doc.email) emit(doc.email, doc); }"
}
}
}'
echo "Creating sample user document..."
curl -X POST http://$COUCHDB_USER:$COUCHDB_PASSWORD@couchdb-service:5984/meds_app \
-H "Content-Type: application/json" \
-d '{
"type": "user",
"name": "sample_user",
"email": "user@example.com",
"createdAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}'
echo "Database seeding completed with default admin user"
restartPolicy: Never
backoffLimit: 4
+46
View File
@@ -0,0 +1,46 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP_NAME}-frontend
labels:
app: ${APP_NAME}
component: frontend
spec:
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
component: frontend
template:
metadata:
labels:
app: ${APP_NAME}
component: frontend
spec:
containers:
- name: frontend
image: ${DOCKER_IMAGE}
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: ${APP_NAME}-config
resources:
requests:
memory: '32Mi'
cpu: '20m'
limits:
memory: '64Mi'
cpu: '40m'
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
+46
View File
@@ -0,0 +1,46 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP_NAME}-frontend
labels:
app: ${APP_NAME}
component: frontend
spec:
replicas: 1
selector:
matchLabels:
app: ${APP_NAME}
component: frontend
template:
metadata:
labels:
app: ${APP_NAME}
component: frontend
spec:
containers:
- name: frontend
image: gitea-http.taildb3494.ts.net/will/meds:latest
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: ${APP_NAME}-config
resources:
requests:
memory: "32Mi"
cpu: "20m"
limits:
memory: "64Mi"
cpu: "40m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}-frontend-service
labels:
app: ${APP_NAME}
component: frontend
spec:
selector:
app: ${APP_NAME}
component: frontend
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
type: ClusterIP
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}-frontend-service
labels:
app: ${APP_NAME}
component: frontend
spec:
selector:
app: ${APP_NAME}
component: frontend
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
type: ClusterIP
+21
View File
@@ -0,0 +1,21 @@
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: frontend-hpa
labels:
app: rxminder
component: frontend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: frontend
minReplicas: 1
maxReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
+29
View File
@@ -0,0 +1,29 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
labels:
app: rxminder
component: frontend
annotations: {}
# Add SSL redirect if using HTTPS
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
# Uncomment for HTTPS with cert-manager
# tls:
# - hosts:
# - ${INGRESS_HOST}
# secretName: frontend-tls
rules:
- host: app.meds.192.168.153.243.nip.io # TODO: Make configurable via deployment script
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
+29
View File
@@ -0,0 +1,29 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ${APP_NAME}-ingress
labels:
app: ${APP_NAME}
component: frontend
annotations:
# Add SSL redirect if using HTTPS
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
# Uncomment for HTTPS with cert-manager
# tls:
# - hosts:
# - ${INGRESS_HOST}
# secretName: frontend-tls
rules:
- host: ${INGRESS_HOST}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ${APP_NAME}-frontend-service
port:
number: 80
+68
View File
@@ -0,0 +1,68 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-policy
labels:
app: rxminder
component: frontend
spec:
podSelector:
matchLabels:
component: frontend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
component: frontend
ports:
- protocol: TCP
port: 80
egress:
- to:
- podSelector:
matchLabels:
component: database
ports:
- protocol: TCP
port: 5984
- to:
- podSelector:
matchLabels:
component: frontend
ports:
- protocol: TCP
port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-policy
labels:
app: rxminder
component: database
spec:
podSelector:
matchLabels:
component: database
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
component: frontend
ports:
- protocol: TCP
port: 5984
egress:
- to:
- podSelector:
matchLabels:
component: database
ports:
- protocol: TCP
port: 5984