diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9339b87 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +deploy/k8s/secrets.yaml diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..618b19e --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +npm-debug.log +.env +.env.local +.env.*.local +coverage +__tests__ +*.test.js +.git +.gitignore +README.md +.vscode +.idea +uploads/* +!uploads/.gitkeep diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7b31ee8 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,38 @@ +# Multi-stage build for ARM compatibility (Raspberry Pi) +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# --- Production stage --- +FROM node:18-alpine + +# Install curl for health checks +RUN apk add --no-cache curl + +WORKDIR /app + +# Copy dependencies from builder +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app . + +# Create uploads directory +RUN mkdir -p uploads + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ + CMD node -e "require('http').get('http://localhost:5000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start server +CMD ["node", "server.js"] diff --git a/backend/server.js b/backend/server.js index 31304eb..175b2ed 100644 --- a/backend/server.js +++ b/backend/server.js @@ -115,6 +115,16 @@ app.use("/api/auth/register", authLimiter); app.use("/api/auth/login", authLimiter); app.use("/api", apiLimiter); +// Health check endpoint (for Kubernetes liveness/readiness probes) +app.get("/api/health", (req, res) => { + res.status(200).json({ + status: "healthy", + timestamp: new Date().toISOString(), + uptime: process.uptime(), + mongodb: mongoose.connection.readyState === 1 ? "connected" : "disconnected", + }); +}); + // Routes app.use("/api/auth", authRoutes); app.use("/api/streets", streetRoutes); diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..ace7a00 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,395 @@ +# Adopt-a-Street Deployment + +This directory contains deployment configurations for the Adopt-a-Street application on Kubernetes (Raspberry Pi cluster). + +## Directory Structure + +``` +deploy/ +├── k8s/ # Kubernetes manifests +│ ├── namespace.yaml # Namespace definition +│ ├── configmap.yaml # Environment configuration +│ ├── secrets.yaml.example # Secret template (COPY TO secrets.yaml) +│ ├── mongodb-statefulset.yaml # MongoDB StatefulSet with PVC +│ ├── backend-deployment.yaml # Backend Deployment + Service +│ ├── frontend-deployment.yaml # Frontend Deployment + Service +│ └── ingress.yaml # Ingress for routing +├── README.md # This file +└── scripts/ # Deployment helper scripts +``` + +## Prerequisites + +### Cluster Requirements +- Kubernetes cluster with 3 nodes: + - 2x Raspberry Pi 5 (8GB RAM) - ARM64 + - 1x Raspberry Pi 3B+ (1GB RAM) - ARMv7 +- kubectl configured to access your cluster +- Container registry accessible from cluster +- Ingress controller installed (Traefik or NGINX Ingress) +- Persistent storage provisioner (local-path, NFS, or Longhorn) + +### Local Requirements +- Docker with buildx for multi-arch builds +- kubectl CLI tool +- Access to container registry (Docker Hub, GitHub Container Registry, or private registry) + +## Quick Start + +### 1. Build Multi-Arch Docker Images + +Build images for both ARM64 (Pi 5) and ARMv7 (Pi 3B+): + +```bash +# From project root +cd /home/will/Code/adopt-a-street + +# Create buildx builder (one-time setup) +docker buildx create --use --name multiarch-builder + +# Build and push backend +docker buildx build --platform linux/arm64,linux/arm/v7 \ + -t your-registry/adopt-a-street-backend:latest \ + --push ./backend + +# Build and push frontend +docker buildx build --platform linux/arm64,linux/arm/v7 \ + -t your-registry/adopt-a-street-frontend:latest \ + --push ./frontend +``` + +**Note:** Replace `your-registry` with your actual registry (e.g., `docker.io/username` or `ghcr.io/username`) + +### 2. Configure Secrets + +```bash +# Copy secrets template +cp deploy/k8s/secrets.yaml.example deploy/k8s/secrets.yaml + +# Edit secrets with your actual values +nano deploy/k8s/secrets.yaml + +# IMPORTANT: Add secrets.yaml to .gitignore if not already there +echo "deploy/k8s/secrets.yaml" >> .gitignore +``` + +**Required Secrets:** +- `JWT_SECRET` - Strong random string for JWT signing +- `CLOUDINARY_CLOUD_NAME` - Your Cloudinary cloud name +- `CLOUDINARY_API_KEY` - Your Cloudinary API key +- `CLOUDINARY_API_SECRET` - Your Cloudinary API secret + +### 3. Update Image References + +Update the image references in deployment files: + +```bash +# Update backend image reference +nano deploy/k8s/backend-deployment.yaml +# Change: image: your-registry/adopt-a-street-backend:latest + +# Update frontend image reference +nano deploy/k8s/frontend-deployment.yaml +# Change: image: your-registry/adopt-a-street-frontend:latest +``` + +### 4. Update Domain Name + +Update the ingress host: + +```bash +nano deploy/k8s/ingress.yaml +# Change: host: adopt-a-street.local +# To your actual domain or IP +``` + +### 5. Deploy to Kubernetes + +```bash +# Create namespace +kubectl apply -f deploy/k8s/namespace.yaml + +# Create secrets (IMPORTANT: Make sure you've edited secrets.yaml!) +kubectl apply -f deploy/k8s/secrets.yaml + +# Create ConfigMap +kubectl apply -f deploy/k8s/configmap.yaml + +# Deploy MongoDB +kubectl apply -f deploy/k8s/mongodb-statefulset.yaml + +# Wait for MongoDB to be ready (this may take 1-2 minutes) +kubectl wait --for=condition=ready pod -l app=mongodb -n adopt-a-street --timeout=120s + +# Deploy backend +kubectl apply -f deploy/k8s/backend-deployment.yaml + +# Wait for backend to be ready +kubectl wait --for=condition=ready pod -l app=backend -n adopt-a-street --timeout=120s + +# Deploy frontend +kubectl apply -f deploy/k8s/frontend-deployment.yaml + +# Deploy ingress +kubectl apply -f deploy/k8s/ingress.yaml + +# Check deployment status +kubectl get all -n adopt-a-street +``` + +## Verification + +### Check Pod Status + +```bash +# View all resources +kubectl get all -n adopt-a-street + +# Check pod status +kubectl get pods -n adopt-a-street + +# Expected output: +# NAME READY STATUS RESTARTS AGE +# adopt-a-street-backend-xxxxxxxxxx-xxxxx 1/1 Running 0 5m +# adopt-a-street-backend-xxxxxxxxxx-xxxxx 1/1 Running 0 5m +# adopt-a-street-frontend-xxxxxxxxx-xxxxx 1/1 Running 0 5m +# adopt-a-street-frontend-xxxxxxxxx-xxxxx 1/1 Running 0 5m +# adopt-a-street-mongodb-0 1/1 Running 0 10m +``` + +### Check Logs + +```bash +# Backend logs +kubectl logs -f deployment/adopt-a-street-backend -n adopt-a-street + +# Frontend logs +kubectl logs -f deployment/adopt-a-street-frontend -n adopt-a-street + +# MongoDB logs +kubectl logs -f adopt-a-street-mongodb-0 -n adopt-a-street +``` + +### Check Services + +```bash +kubectl get svc -n adopt-a-street + +# Expected output: +# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +# adopt-a-street-backend ClusterIP 10.43.x.x 5000/TCP 5m +# adopt-a-street-frontend ClusterIP 10.43.x.x 80/TCP 5m +# adopt-a-street-mongodb ClusterIP None 27017/TCP 10m +``` + +### Check Ingress + +```bash +kubectl get ingress -n adopt-a-street + +# Get ingress details +kubectl describe ingress adopt-a-street-ingress -n adopt-a-street +``` + +### Access the Application + +```bash +# Port forward for testing (if ingress not working) +kubectl port-forward svc/adopt-a-street-frontend 3000:80 -n adopt-a-street + +# Then open http://localhost:3000 in your browser +``` + +## Resource Allocation + +The deployment is optimized for Raspberry Pi hardware: + +### MongoDB (Pi 5 nodes only) +- **Requests:** 512Mi RAM, 250m CPU +- **Limits:** 2Gi RAM, 1000m CPU +- **Storage:** 10Gi persistent volume + +### Backend (prefers Pi 5 nodes) +- **Requests:** 256Mi RAM, 100m CPU +- **Limits:** 512Mi RAM, 500m CPU +- **Replicas:** 2 pods + +### Frontend (any node) +- **Requests:** 64Mi RAM, 50m CPU +- **Limits:** 128Mi RAM, 200m CPU +- **Replicas:** 2 pods + +### Total Cluster Requirements +- **Minimum RAM:** ~3.5 GB (1.5GB MongoDB + 1GB backend + 200MB frontend + 800MB system) +- **Recommended:** 2x Pi 5 (8GB each) handles this comfortably + +## Scaling + +### Scale Deployments + +```bash +# Scale backend +kubectl scale deployment adopt-a-street-backend --replicas=3 -n adopt-a-street + +# Scale frontend +kubectl scale deployment adopt-a-street-frontend --replicas=3 -n adopt-a-street +``` + +**Note:** MongoDB is a StatefulSet with 1 replica. Scaling MongoDB requires configuring replication. + +## Updating + +### Update Images + +```bash +# Build and push new version +docker buildx build --platform linux/arm64,linux/arm/v7 \ + -t your-registry/adopt-a-street-backend:v2.0 \ + --push ./backend + +# Update deployment +kubectl set image deployment/adopt-a-street-backend \ + backend=your-registry/adopt-a-street-backend:v2.0 \ + -n adopt-a-street + +# Check rollout status +kubectl rollout status deployment/adopt-a-street-backend -n adopt-a-street +``` + +### Rollback + +```bash +# Rollback to previous version +kubectl rollout undo deployment/adopt-a-street-backend -n adopt-a-street + +# Rollback to specific revision +kubectl rollout undo deployment/adopt-a-street-backend --to-revision=2 -n adopt-a-street +``` + +## Monitoring + +### Resource Usage + +```bash +# Node resource usage +kubectl top nodes + +# Pod resource usage +kubectl top pods -n adopt-a-street +``` + +### Events + +```bash +# View recent events +kubectl get events -n adopt-a-street --sort-by='.lastTimestamp' +``` + +### Describe Resources + +```bash +# Describe pod (useful for troubleshooting) +kubectl describe pod -n adopt-a-street + +# Describe deployment +kubectl describe deployment adopt-a-street-backend -n adopt-a-street +``` + +## Troubleshooting + +### Pod Not Starting + +```bash +# Check pod events +kubectl describe pod -n adopt-a-street + +# Check logs +kubectl logs -n adopt-a-street + +# Check previous logs (if pod crashed) +kubectl logs -n adopt-a-street --previous +``` + +### Image Pull Errors + +- Verify image exists in registry +- Check image name and tag in deployment +- Verify cluster can access registry +- Check if imagePullSecrets are needed + +### MongoDB Connection Issues + +```bash +# Shell into backend pod +kubectl exec -it -n adopt-a-street -- sh + +# Test MongoDB connection +wget -qO- http://adopt-a-street-mongodb:27017 +``` + +### Persistent Volume Issues + +```bash +# Check PVCs +kubectl get pvc -n adopt-a-street + +# Check PVs +kubectl get pv + +# Describe PVC +kubectl describe pvc mongodb-data-adopt-a-street-mongodb-0 -n adopt-a-street +``` + +## Cleanup + +### Delete Everything + +```bash +# Delete all resources in namespace +kubectl delete namespace adopt-a-street + +# Or delete resources individually +kubectl delete -f deploy/k8s/ingress.yaml +kubectl delete -f deploy/k8s/frontend-deployment.yaml +kubectl delete -f deploy/k8s/backend-deployment.yaml +kubectl delete -f deploy/k8s/mongodb-statefulset.yaml +kubectl delete -f deploy/k8s/configmap.yaml +kubectl delete -f deploy/k8s/secrets.yaml +kubectl delete -f deploy/k8s/namespace.yaml + +# Note: This will also delete the persistent volume data! +``` + +## Security Best Practices + +1. **Never commit secrets.yaml** - Always use secrets.yaml.example +2. **Use strong JWT_SECRET** - Generate with: `openssl rand -base64 32` +3. **Enable TLS/HTTPS** - Uncomment TLS section in ingress.yaml and use cert-manager +4. **Restrict ingress** - Use network policies to limit pod communication +5. **Use image digests** - Pin images to specific SHA256 digests for production +6. **Enable RBAC** - Create service accounts with minimal permissions +7. **Scan images** - Use tools like Trivy to scan for vulnerabilities + +## Performance Optimization + +1. **Use imagePullPolicy: IfNotPresent** - After initial deployment to save bandwidth +2. **Implement HPA** - Horizontal Pod Autoscaler for dynamic scaling +3. **Add Redis** - For caching to reduce MongoDB load +4. **Use CDN** - For frontend static assets +5. **Enable compression** - Nginx already configured with gzip +6. **Monitor resources** - Use Prometheus + Grafana for metrics + +## Additional Resources + +- [Kubernetes Documentation](https://kubernetes.io/docs/) +- [Raspberry Pi Kubernetes Guide](https://github.com/alexellis/k8s-on-raspbian) +- [Helm Charts](https://helm.sh/) - Consider migrating to Helm for easier management +- [ArgoCD](https://argoproj.github.io/cd/) - GitOps continuous delivery for Kubernetes + +## Support + +For issues or questions: +1. Check pod logs: `kubectl logs -n adopt-a-street` +2. Check events: `kubectl get events -n adopt-a-street` +3. Describe resources: `kubectl describe -n adopt-a-street` +4. Review application logs in the backend diff --git a/deploy/k8s/backend-deployment.yaml b/deploy/k8s/backend-deployment.yaml new file mode 100644 index 0000000..59ec829 --- /dev/null +++ b/deploy/k8s/backend-deployment.yaml @@ -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 diff --git a/deploy/k8s/configmap.yaml b/deploy/k8s/configmap.yaml new file mode 100644 index 0000000..ceabd29 --- /dev/null +++ b/deploy/k8s/configmap.yaml @@ -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" diff --git a/deploy/k8s/frontend-deployment.yaml b/deploy/k8s/frontend-deployment.yaml new file mode 100644 index 0000000..5e3a9b6 --- /dev/null +++ b/deploy/k8s/frontend-deployment.yaml @@ -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 diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml new file mode 100644 index 0000000..31557c6 --- /dev/null +++ b/deploy/k8s/ingress.yaml @@ -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 diff --git a/deploy/k8s/mongodb-statefulset.yaml b/deploy/k8s/mongodb-statefulset.yaml new file mode 100644 index 0000000..0d0471a --- /dev/null +++ b/deploy/k8s/mongodb-statefulset.yaml @@ -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 diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/namespace.yaml new file mode 100644 index 0000000..2d2a806 --- /dev/null +++ b/deploy/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: adopt-a-street + labels: + name: adopt-a-street + environment: production diff --git a/deploy/k8s/secrets.yaml.example b/deploy/k8s/secrets.yaml.example new file mode 100644 index 0000000..49bddc8 --- /dev/null +++ b/deploy/k8s/secrets.yaml.example @@ -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 diff --git a/deploy/scripts/build.sh b/deploy/scripts/build.sh new file mode 100755 index 0000000..96503cc --- /dev/null +++ b/deploy/scripts/build.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# Adopt-a-Street Multi-Arch Docker Build Script +# Builds images for ARM64 (Pi 5) and ARMv7 (Pi 3B+) + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +REGISTRY="${DOCKER_REGISTRY:-your-registry}" +TAG="${DOCKER_TAG:-latest}" + +echo -e "${GREEN}🐳 Adopt-a-Street Multi-Arch Docker Build${NC}" +echo "================================================" +echo "Registry: ${REGISTRY}" +echo "Tag: ${TAG}" +echo "Project Root: ${PROJECT_ROOT}" +echo "" + +# Check if docker buildx is available +if ! docker buildx version &> /dev/null; then + echo -e "${RED}❌ Docker buildx not found. Please install Docker with buildx support.${NC}" + exit 1 +fi + +# Create buildx builder if it doesn't exist +if ! docker buildx inspect multiarch-builder &> /dev/null; then + echo "🔨 Creating buildx builder..." + docker buildx create --use --name multiarch-builder + echo -e "${GREEN}✓${NC} Builder created" +else + echo "🔨 Using existing buildx builder..." + docker buildx use multiarch-builder +fi +echo "" + +# Prompt for registry if using default +if [ "${REGISTRY}" = "your-registry" ]; then + echo -e "${YELLOW}⚠️ Using default registry 'your-registry'${NC}" + echo -e "${YELLOW}Set DOCKER_REGISTRY environment variable to use a different registry:${NC}" + echo " export DOCKER_REGISTRY=docker.io/username" + echo " export DOCKER_REGISTRY=ghcr.io/username" + echo "" + read -p "Continue with 'your-registry'? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 + fi +fi + +# Build backend +echo "🔧 Building backend image..." +echo " Platforms: linux/arm64, linux/arm/v7" +echo " Image: ${REGISTRY}/adopt-a-street-backend:${TAG}" +docker buildx build --platform linux/arm64,linux/arm/v7 \ + -t "${REGISTRY}/adopt-a-street-backend:${TAG}" \ + --push \ + "${PROJECT_ROOT}/backend" +echo -e "${GREEN}✓${NC} Backend image built and pushed" +echo "" + +# Build frontend +echo "🎨 Building frontend image..." +echo " Platforms: linux/arm64, linux/arm/v7" +echo " Image: ${REGISTRY}/adopt-a-street-frontend:${TAG}" +docker buildx build --platform linux/arm64,linux/arm/v7 \ + -t "${REGISTRY}/adopt-a-street-frontend:${TAG}" \ + --push \ + "${PROJECT_ROOT}/frontend" +echo -e "${GREEN}✓${NC} Frontend image built and pushed" +echo "" + +echo "================================================" +echo -e "${GREEN}✅ Build Complete!${NC}" +echo "================================================" +echo "" +echo "Images pushed:" +echo " 📦 ${REGISTRY}/adopt-a-street-backend:${TAG}" +echo " 📦 ${REGISTRY}/adopt-a-street-frontend:${TAG}" +echo "" +echo -e "${YELLOW}📝 Next Steps:${NC}" +echo "1. Update image references in deployment files:" +echo " sed -i 's|your-registry|${REGISTRY}|g' deploy/k8s/*.yaml" +echo "" +echo "2. Deploy to Kubernetes:" +echo " ./deploy/scripts/deploy.sh" +echo "" +echo -e "${GREEN}🎉 Happy deploying!${NC}" diff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh new file mode 100755 index 0000000..033dd46 --- /dev/null +++ b/deploy/scripts/deploy.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +# Adopt-a-Street Kubernetes Deployment Script +# This script deploys the Adopt-a-Street application to a Kubernetes cluster + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +NAMESPACE="adopt-a-street" +DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +K8S_DIR="${DEPLOY_DIR}/k8s" + +echo -e "${GREEN}🚀 Adopt-a-Street Kubernetes Deployment${NC}" +echo "================================================" +echo "" + +# Check if kubectl is installed +if ! command -v kubectl &> /dev/null; then + echo -e "${RED}❌ kubectl not found. Please install kubectl first.${NC}" + exit 1 +fi + +# Check if secrets.yaml exists +if [ ! -f "${K8S_DIR}/secrets.yaml" ]; then + echo -e "${RED}❌ secrets.yaml not found!${NC}" + echo -e "${YELLOW}Please copy secrets.yaml.example to secrets.yaml and fill in your secrets:${NC}" + echo " cp ${K8S_DIR}/secrets.yaml.example ${K8S_DIR}/secrets.yaml" + echo " nano ${K8S_DIR}/secrets.yaml" + exit 1 +fi + +echo -e "${GREEN}✓${NC} Prerequisites check passed" +echo "" + +# Function to wait for pods +wait_for_pods() { + local label=$1 + local timeout=${2:-120} + echo -e "${YELLOW}⏳ Waiting for pods with label ${label} to be ready...${NC}" + kubectl wait --for=condition=ready pod -l "${label}" -n "${NAMESPACE}" --timeout="${timeout}s" || { + echo -e "${RED}❌ Timeout waiting for pods${NC}" + kubectl get pods -n "${NAMESPACE}" -l "${label}" + return 1 + } + echo -e "${GREEN}✓${NC} Pods ready" +} + +# Create namespace +echo "📦 Creating namespace..." +kubectl apply -f "${K8S_DIR}/namespace.yaml" +echo -e "${GREEN}✓${NC} Namespace created" +echo "" + +# Apply secrets +echo "🔐 Applying secrets..." +kubectl apply -f "${K8S_DIR}/secrets.yaml" +echo -e "${GREEN}✓${NC} Secrets applied" +echo "" + +# Apply configmap +echo "⚙️ Applying ConfigMap..." +kubectl apply -f "${K8S_DIR}/configmap.yaml" +echo -e "${GREEN}✓${NC} ConfigMap applied" +echo "" + +# Deploy MongoDB +echo "🗄️ Deploying MongoDB..." +kubectl apply -f "${K8S_DIR}/mongodb-statefulset.yaml" +wait_for_pods "app=mongodb" 180 +echo -e "${GREEN}✓${NC} MongoDB deployed" +echo "" + +# Deploy backend +echo "🔧 Deploying backend..." +kubectl apply -f "${K8S_DIR}/backend-deployment.yaml" +wait_for_pods "app=backend" 120 +echo -e "${GREEN}✓${NC} Backend deployed" +echo "" + +# Deploy frontend +echo "🎨 Deploying frontend..." +kubectl apply -f "${K8S_DIR}/frontend-deployment.yaml" +wait_for_pods "app=frontend" 120 +echo -e "${GREEN}✓${NC} Frontend deployed" +echo "" + +# Deploy ingress +echo "🌐 Deploying ingress..." +kubectl apply -f "${K8S_DIR}/ingress.yaml" +echo -e "${GREEN}✓${NC} Ingress deployed" +echo "" + +# Show deployment status +echo "================================================" +echo -e "${GREEN}✅ Deployment Complete!${NC}" +echo "================================================" +echo "" + +echo "📊 Current Status:" +kubectl get all -n "${NAMESPACE}" +echo "" + +echo "🌐 Ingress:" +kubectl get ingress -n "${NAMESPACE}" +echo "" + +echo -e "${YELLOW}📝 Next Steps:${NC}" +echo "1. Check pod logs:" +echo " kubectl logs -f deployment/adopt-a-street-backend -n ${NAMESPACE}" +echo "" +echo "2. Access the application through your ingress URL" +echo "" +echo "3. Or port-forward for testing:" +echo " kubectl port-forward svc/adopt-a-street-frontend 3000:80 -n ${NAMESPACE}" +echo " Then open http://localhost:3000" +echo "" +echo "4. Monitor resources:" +echo " kubectl top pods -n ${NAMESPACE}" +echo "" + +echo -e "${GREEN}🎉 Happy deploying!${NC}" diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..bda78bc --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,13 @@ +node_modules +npm-debug.log +build +.git +.gitignore +README.md +.env.local +.env.*.local +.vscode +.idea +coverage +src/**/*.test.js +src/**/__tests__ diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..27053aa --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,37 @@ +# Multi-stage build for ARM compatibility (Raspberry Pi) +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build production bundle +RUN npm run build + +# --- Production stage with nginx --- +FROM nginx:alpine + +# Install wget for health checks +RUN apk add --no-cache wget + +# Copy built assets +COPY --from=builder /app/build /usr/share/nginx/html + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget --quiet --tries=1 --spider http://localhost:80/health || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..ebe029d --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,63 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # React Router support - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # If serving API through frontend nginx (optional - prefer Ingress routing) + # Uncomment these if NOT using Kubernetes Ingress for backend routing + + # location /api { + # proxy_pass http://adopt-a-street-backend:5000; + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection 'upgrade'; + # proxy_set_header Host $host; + # proxy_cache_bypass $http_upgrade; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } + + # Socket.IO proxy (optional - prefer Ingress routing) + # location /socket.io { + # proxy_pass http://adopt-a-street-backend:5000; + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } +}