feat: add complete Kubernetes deployment infrastructure
Add production-ready deployment configuration for Raspberry Pi cluster with comprehensive documentation and automation scripts. Kubernetes Manifests (deploy/k8s/): - namespace.yaml - Dedicated namespace for the application - configmap.yaml - Environment configuration (MongoDB URI, ports, URLs) - secrets.yaml.example - Template for sensitive credentials (JWT, Cloudinary, Stripe) - mongodb-statefulset.yaml - MongoDB with persistent storage, placed on Pi 5 nodes (ARM64) - backend-deployment.yaml - Backend with 2 replicas, prefers Pi 5 nodes, health checks - frontend-deployment.yaml - Frontend with 2 replicas, can run on any node, nginx-based - ingress.yaml - Traefik/NGINX ingress for API, Socket.IO, and frontend routing Docker Configuration: - backend/Dockerfile - Multi-stage build for ARM64/ARMv7 with health checks - backend/.dockerignore - Excludes tests, coverage, node_modules from build - frontend/Dockerfile - Multi-stage build with nginx, optimized for ARM - frontend/.dockerignore - Excludes dev files from production build - frontend/nginx.conf - Production nginx config with gzip, caching, React Router support Resource Optimization for Pi Cluster: - MongoDB: 512Mi-2Gi RAM, 250m-1000m CPU (Pi 5 only, ARM64 affinity) - Backend: 256Mi-512Mi RAM, 100m-500m CPU (prefers Pi 5, ARM64) - Frontend: 64Mi-128Mi RAM, 50m-200m CPU (any node, lightweight) - Total: ~3.5GB RAM minimum, perfect for 2x Pi 5 (8GB) + 1x Pi 3B+ (1GB) Automation Scripts (deploy/scripts/): - build.sh - Build multi-arch images (ARM64/ARMv7) and push to registry - deploy.sh - Deploy all Kubernetes resources with health checks and status reporting - Both scripts include error handling, color output, and comprehensive logging Documentation (deploy/README.md): - Complete deployment guide with prerequisites - Step-by-step instructions for building and deploying - Verification commands and troubleshooting guide - Scaling, updating, and rollback procedures - Resource monitoring and cleanup instructions - Security best practices and performance optimization tips Health Endpoints: - Backend: GET /api/health (status, uptime, MongoDB connection) - Frontend: GET /health (nginx health check) - Used by Kubernetes liveness and readiness probes Key Features: - Multi-architecture support (ARM64 for Pi 5, ARMv7 for Pi 3B+) - NodeAffinity places heavy workloads (MongoDB, backend) on Pi 5 nodes - Persistent storage for MongoDB (10Gi PVC) - Horizontal pod autoscaling ready - Zero-downtime deployments with rolling updates - Comprehensive health monitoring - Production-grade nginx with security headers - Ingress routing for API, WebSocket, and static assets Security: - Secrets management with Kubernetes Secrets - secrets.yaml excluded from Git (.gitignore) - Minimal container images (alpine-based) - Health checks prevent unhealthy pods from serving traffic - Security headers in nginx (X-Frame-Options, X-Content-Type-Options, etc.) Usage: 1. Build images: ./deploy/scripts/build.sh 2. Configure secrets: cp deploy/k8s/secrets.yaml.example deploy/k8s/secrets.yaml 3. Deploy: ./deploy/scripts/deploy.sh 4. Monitor: kubectl get all -n adopt-a-street 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
79
deploy/k8s/backend-deployment.yaml
Normal file
79
deploy/k8s/backend-deployment.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: adopt-a-street-backend
|
||||
namespace: adopt-a-street
|
||||
labels:
|
||||
app: backend
|
||||
spec:
|
||||
selector:
|
||||
app: backend
|
||||
ports:
|
||||
- port: 5000
|
||||
targetPort: 5000
|
||||
name: http
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: adopt-a-street-backend
|
||||
namespace: adopt-a-street
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backend
|
||||
spec:
|
||||
# Prefer Pi 5 nodes for backend (more RAM for Node.js)
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: kubernetes.io/arch
|
||||
operator: In
|
||||
values:
|
||||
- arm64 # Pi 5 architecture
|
||||
containers:
|
||||
- name: backend
|
||||
# Update with your registry and tag
|
||||
image: your-registry/adopt-a-street-backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
name: http
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: adopt-a-street-config
|
||||
- secretRef:
|
||||
name: adopt-a-street-secrets
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 5000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 5000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
15
deploy/k8s/configmap.yaml
Normal file
15
deploy/k8s/configmap.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: adopt-a-street-config
|
||||
namespace: adopt-a-street
|
||||
data:
|
||||
# MongoDB Connection
|
||||
MONGO_URI: "mongodb://adopt-a-street-mongodb:27017/adopt-a-street"
|
||||
|
||||
# Backend Configuration
|
||||
PORT: "5000"
|
||||
NODE_ENV: "production"
|
||||
|
||||
# Frontend URL (update with your actual domain)
|
||||
FRONTEND_URL: "http://adopt-a-street.local"
|
||||
64
deploy/k8s/frontend-deployment.yaml
Normal file
64
deploy/k8s/frontend-deployment.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: adopt-a-street-frontend
|
||||
namespace: adopt-a-street
|
||||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
selector:
|
||||
app: frontend
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: adopt-a-street-frontend
|
||||
namespace: adopt-a-street
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontend
|
||||
spec:
|
||||
# Frontend can run on any node (lightweight static serving)
|
||||
containers:
|
||||
- name: frontend
|
||||
# Update with your registry and tag
|
||||
image: your-registry/adopt-a-street-frontend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "200m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 80
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
53
deploy/k8s/ingress.yaml
Normal file
53
deploy/k8s/ingress.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: adopt-a-street-ingress
|
||||
namespace: adopt-a-street
|
||||
annotations:
|
||||
# Uncomment the appropriate ingress class for your cluster
|
||||
kubernetes.io/ingress.class: "traefik" # For Traefik
|
||||
# kubernetes.io/ingress.class: "nginx" # For NGINX Ingress
|
||||
|
||||
# Uncomment if using cert-manager for TLS
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
|
||||
# Traefik specific annotations (uncomment if using Traefik)
|
||||
# traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||
# traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
|
||||
spec:
|
||||
rules:
|
||||
- host: adopt-a-street.local # CHANGE THIS to your actual domain
|
||||
http:
|
||||
paths:
|
||||
# API endpoints
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: adopt-a-street-backend
|
||||
port:
|
||||
number: 5000
|
||||
|
||||
# Socket.IO endpoints
|
||||
- path: /socket.io
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: adopt-a-street-backend
|
||||
port:
|
||||
number: 5000
|
||||
|
||||
# Frontend (must be last - catches all other paths)
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: adopt-a-street-frontend
|
||||
port:
|
||||
number: 80
|
||||
|
||||
# Uncomment for TLS/HTTPS
|
||||
# tls:
|
||||
# - hosts:
|
||||
# - adopt-a-street.local
|
||||
# secretName: adopt-a-street-tls
|
||||
89
deploy/k8s/mongodb-statefulset.yaml
Normal file
89
deploy/k8s/mongodb-statefulset.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: adopt-a-street-mongodb
|
||||
namespace: adopt-a-street
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
clusterIP: None # Headless service for StatefulSet
|
||||
selector:
|
||||
app: mongodb
|
||||
ports:
|
||||
- port: 27017
|
||||
targetPort: 27017
|
||||
name: mongodb
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: adopt-a-street-mongodb
|
||||
namespace: adopt-a-street
|
||||
spec:
|
||||
serviceName: adopt-a-street-mongodb
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mongodb
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
# Place MongoDB on Pi 5 nodes (more RAM)
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/arch
|
||||
operator: In
|
||||
values:
|
||||
- arm64 # Pi 5 architecture
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: mongo:7.0
|
||||
ports:
|
||||
- containerPort: 27017
|
||||
name: mongodb
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
volumeMounts:
|
||||
- name: mongodb-data
|
||||
mountPath: /data/db
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mongosh
|
||||
- --eval
|
||||
- "db.adminCommand('ping')"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mongosh
|
||||
- --eval
|
||||
- "db.adminCommand('ping')"
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: mongodb-data
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
# Uncomment and set your storage class if needed
|
||||
# storageClassName: local-path
|
||||
7
deploy/k8s/namespace.yaml
Normal file
7
deploy/k8s/namespace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: adopt-a-street
|
||||
labels:
|
||||
name: adopt-a-street
|
||||
environment: production
|
||||
24
deploy/k8s/secrets.yaml.example
Normal file
24
deploy/k8s/secrets.yaml.example
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: adopt-a-street-secrets
|
||||
namespace: adopt-a-street
|
||||
type: Opaque
|
||||
stringData:
|
||||
# JWT Secret - CHANGE THIS IN PRODUCTION!
|
||||
JWT_SECRET: "your-super-secret-jwt-key-change-in-production"
|
||||
|
||||
# Cloudinary Configuration
|
||||
CLOUDINARY_CLOUD_NAME: "your-cloudinary-cloud-name"
|
||||
CLOUDINARY_API_KEY: "your-cloudinary-api-key"
|
||||
CLOUDINARY_API_SECRET: "your-cloudinary-api-secret"
|
||||
|
||||
# Stripe Configuration (optional - currently mocked)
|
||||
# STRIPE_SECRET_KEY: "your-stripe-secret-key"
|
||||
|
||||
---
|
||||
# IMPORTANT:
|
||||
# 1. Copy this file to secrets.yaml
|
||||
# 2. Replace all placeholder values with real secrets
|
||||
# 3. DO NOT commit secrets.yaml to version control
|
||||
# 4. Add secrets.yaml to .gitignore
|
||||
Reference in New Issue
Block a user