Compare commits

...

8 Commits

Author SHA1 Message Date
b3b7834166 added ingress controller 2025-05-21 22:26:43 +02:00
85e761d0d6 update k8s config 2025-05-21 22:05:31 +02:00
f2ff6a2aa2 cleanup 2025-05-21 10:17:30 +02:00
77f8e1554c revised complete k8s deployment 2025-05-21 10:13:38 +02:00
0167ffdd18 Save current state before cleaning history 2025-05-21 09:27:45 +02:00
7f463ee287 fixed readme upstream 2025-05-21 02:14:45 +02:00
797fc34016 Merge branch 'main' into k8s-deployment 2025-05-21 02:12:52 +02:00
ad70178202 update readme 2025-05-20 13:00:00 +02:00
22 changed files with 892 additions and 139 deletions

@ -1,12 +1,97 @@
image: lachlanevenson/k8s-helm:latest
version: 1.0
stages:
- deploy
workflow:
name: Deploy to Production
on:
push:
branches:
- main
variables:
KUBECONFIG: /root/.kube/config # Adjust if you're mounting a kubeconfig differently
jobs:
build-and-deploy:
name: Build and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
deploy:
stage: deploy
script:
- helm upgrade --install fusero ./chart -f ./chart/values-prod.yaml
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: registry.liquidrinu.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and Push Backend
uses: docker/build-push-action@v4
with:
context: ./backend
push: true
tags: registry.liquidrinu.com/fusero-backend:latest
cache-from: type=registry,ref=registry.liquidrinu.com/fusero-backend:buildcache
cache-to: type=registry,ref=registry.liquidrinu.com/fusero-backend:buildcache,mode=max
- name: Build and Push Frontend
uses: docker/build-push-action@v4
with:
context: ./frontend
push: true
tags: registry.liquidrinu.com/fusero-frontend:latest
cache-from: type=registry,ref=registry.liquidrinu.com/fusero-frontend:buildcache
cache-to: type=registry,ref=registry.liquidrinu.com/fusero-frontend:buildcache,mode=max
- name: Install kubectl
uses: azure/setup-kubectl@v3
with:
version: "latest"
- name: Setup kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config
chmod 600 $HOME/.kube/config
- name: Create secrets file
run: |
cat > ./chart/secrets.prod.yaml << EOF
backend:
env:
POSTGRES_PASSWORD: "${{ secrets.POSTGRES_PASSWORD }}"
DEFAULT_ADMIN_PASSWORD: "${{ secrets.DEFAULT_ADMIN_PASSWORD }}"
ENCRYPTION_KEY: "${{ secrets.ENCRYPTION_KEY }}"
JWT_SECRET: "${{ secrets.JWT_SECRET }}"
CHATGPT_API_KEY: "${{ secrets.CHATGPT_API_KEY }}"
CANVAS_API_KEY: "${{ secrets.CANVAS_API_KEY }}"
EOF
- name: Delete old migration/seed job
run: |
kubectl delete job fusero-backend-db-init -n fusero-prod || true
- name: Deploy to Kubernetes
run: |
helm upgrade --install fusero ./chart \
--namespace fusero-prod \
--create-namespace \
--values ./chart/values.prod.yaml \
--values ./chart/secrets.prod.yaml \
--set backend.image.repository=registry.liquidrinu.com/fusero-backend \
--set frontend.image.repository=registry.liquidrinu.com/fusero-frontend
- name: Wait for migration/seed job
run: |
kubectl wait --for=condition=complete --timeout=300s job/fusero-backend-db-init -n fusero-prod
JOB_STATUS=$(kubectl get job fusero-backend-db-init -n fusero-prod -o jsonpath='{.status.succeeded}')
if [ "$JOB_STATUS" != "1" ]; then
echo "Migration/seed job failed!" >&2
kubectl logs job/fusero-backend-db-init -n fusero-prod
exit 1
fi
- name: Verify Deployment
run: |
kubectl rollout status deployment/fusero-backend -n fusero-prod
kubectl rollout status deployment/fusero-frontend -n fusero-prod

19
.gitignore vendored

@ -124,3 +124,22 @@ secrets.yml
values.dev.*
values.prod.*
# Secrets
chart/secrets.prod.yaml
chart/secrets.*.yaml
*.env
.env.*
# Development values with secrets
chart/values.dev.yaml
# Production secrets
chart/secrets.prod.yaml
chart/secrets.*.yaml
# Keep templates and public configs
!chart/secrets.prod.template.yaml
!chart/values.prod.template.yaml
!chart/values.prod.public.yaml
.bkup/

387
README.md

@ -11,6 +11,10 @@ A full-stack application boilerplate with a React frontend and Node.js backend
- [📁 Project Structure](#-project-structure)
- [⚙️ Prerequisites](#-prerequisites)
- [💻 Development Setup](#-development-setup)
- [To create a new migration:](#to-create-a-new-migration)
- [npm run migration:create](#npm-run-migrationcreate)
- [To apply migrations:](#to-apply-migrations)
- [To seed the database:](#to-seed-the-database)
- [Alternate: Running Services in Separate Terminals](#alternate-running-services-in-separate-terminals)
- [🛠️ Environment Setup](#-environment-setup)
- [For Kubernetes, these are set in chart/values.yaml:](#for-kubernetes-these-are-set-in-chartvaluesyaml)
@ -19,7 +23,12 @@ A full-stack application boilerplate with a React frontend and Node.js backend
- [POSTGRES\_PORT=19095](#postgres_port19095)
- [POSTGRES\_USER=root](#postgres_userroot)
- [POSTGRES\_PASSWORD=root123](#postgres_passwordroot123)
- [🚀 Production Deployment](#-production-deployment)
- [🐳 Docker Development](#-docker-development)
- [To create a new migration:](#to-create-a-new-migration-1)
- [npm run migration:create](#npm-run-migrationcreate-1)
- [To apply migrations:](#to-apply-migrations-1)
- [To seed the database:](#to-seed-the-database-1)
- [🚀 Kubernetes Deployment](#-kubernetes-deployment)
- [🌐 Frontend Routing in Production](#-frontend-routing-in-production)
- [🔐 HTTPS with Self-Signed Certificates](#-https-with-self-signed-certificates)
- [🧠 Development Best Practices](#-development-best-practices)
@ -44,6 +53,7 @@ A full-stack application boilerplate with a React frontend and Node.js backend
- [Port-Forwarding for Local Access](#port-forwarding-for-local-access)
- [Frontend (React app)](#frontend-react-app)
- [Backend (API)](#backend-api)
- [Database](#database)
- [NGINX Backend Service Name: Docker Compose vs Kubernetes](#nginx-backend-service-name-docker-compose-vs-kubernetes)
- [How to update the NGINX config for Kubernetes](#how-to-update-the-nginx-config-for-kubernetes)
- [Cleaning Up Duplicate or Crashing Deployments and Pods in Kubernetes](#cleaning-up-duplicate-or-crashing-deployments-and-pods-in-kubernetes)
@ -53,17 +63,68 @@ A full-stack application boilerplate with a React frontend and Node.js backend
- [Debugging Frontend Pod Crashes: NGINX SSL Certificate Errors](#debugging-frontend-pod-crashes-nginx-ssl-certificate-errors)
- [How to fix for Kubernetes (Recommended)](#how-to-fix-for-kubernetes-recommended)
- [Connecting to the Database from Your Host (DBeaver, etc.)](#connecting-to-the-database-from-your-host-dbeaver-etc)
- [🎯 Kubernetes Namespace Management](#-kubernetes-namespace-management)
- [Development Namespace Setup](#development-namespace-setup)
- [Production Namespace Setup](#production-namespace-setup)
- [Namespace Management Commands](#namespace-management-commands)
- [Recommended Kubernetes GUI Tools](#recommended-kubernetes-gui-tools)
- [🆕 Namespaced Development Environment with Helm](#-namespaced-development-environment-with-helm)
- [What was changed:](#what-was-changed)
- [How to use:](#how-to-use)
- [Why use namespaces?](#why-use-namespaces)
- [🔒 Production Security \& Best Practices](#-production-security--best-practices)
- [Environment Variables \& Secrets](#environment-variables--secrets)
- [HTTPS \& Certificates](#https--certificates)
- [CORS \& Security Headers](#cors--security-headers)
- [Logging \& Monitoring](#logging--monitoring)
- [Database Backup](#database-backup)
- [CI/CD \& Automated Testing](#cicd--automated-testing)
- [Troubleshooting Production](#troubleshooting-production)
- [🆕 Recent Improvements \& Troubleshooting](#-recent-improvements--troubleshooting)
- [🚀 Production Deployment Pipeline (CI/CD)](#-production-deployment-pipeline-cicd)
---
## 📁 Project Structure
fusero-app-boilerplate/
├── frontend/ # React frontend application
├── backend/ # Node.js backend application
├── docker-compose.yml # Production Docker configuration
├── docker-compose.dev.yml # Development Docker configuration
└── chart/ # Helm chart for Kubernetes deployment
fusero-app-boilerplate/
├── chart/ # Helm chart for Kubernetes
│ ├── Chart.yaml
│ ├── values.dev.yaml
│ ├── values.prod.yaml
│ └── templates/
├── config/
├── coverage/
├── dist/
├── docs/
├── frontend/ # React frontend app
│ ├── public/
│ └── src/
├── mikro-orm.config.ts
├── nginx/
├── node_modules/
├── package.json
├── package-lock.json
├── docker-compose.yaml
├── docker-compose.dev.yaml
├── .gitignore
├── .gitea-ci.yaml
├── .prettierrc.json
├── .eslintrc.json
├── architecture.excalidraw
├── src/ # Node.js backend source
│ ├── apps/
│ ├── constants/
│ ├── database/
│ ├── middleware/
│ ├── plugins/
│ ├── shared/
│ ├── tests/
│ ├── types/
│ └── ...
├── test/
├── utils/
└── README.md
---
@ -81,14 +142,18 @@ fusero-app-boilerplate/
🗃️ PostgreSQL must run in Docker for consistent behavior.
Create volume and start the database:
docker volume create fusero-db-data
docker volume create fusero-dev-db-data
docker-compose up -d db
Backend setup:
cd backend
cp .env.example .env
npm install
# To create a new migration:
# npm run migration:create
# To apply migrations:
npm run migrate
# To seed the database:
npm run seed
npm run dev &
cd ..
@ -142,17 +207,50 @@ VITE_API_BASE_URL=http://localhost:14000/api/v1
---
## 🚀 Production Deployment
## 🐳 Docker Development
🗃️ PostgreSQL must run in Docker for consistent behavior.
Create volume and start the database:
docker volume create fusero-dev-db-data
docker-compose up -d db
Backend setup:
cd backend
cp .env.example .env
npm install
# To create a new migration:
# npm run migration:create
# To apply migrations:
npm run migrate
# To seed the database:
npm run seed
npm run dev &
cd ..
Frontend setup:
cd frontend
cp .env.example .env
npm install
npm run dev &
cd ..
App is running:
Frontend → http://localhost:3000
Backend → http://localhost:14000
---
## 🚀 Kubernetes Deployment
1. Build and run with Docker:
docker-compose up --build
2. Apply migrations and seed inside backend container:
docker exec -it fusero-app-backend npx mikro-orm migration:up
docker exec -it fusero-app-backend npm run migrate
docker exec -it fusero-app-backend npm run seed
3. Ensure all required environment variables are configured.
Never commit `.env` files.
---
@ -179,7 +277,7 @@ Wrong: to="/dashboard/canvas/canvas-endpoints"
Generate a self-signed cert:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./nginx/ssl/nginx.key -out ./nginx/ssl/nginx.crt
Ensure `docker-compose.yml` mounts the certs:
Ensure `docker-compose.yaml` mounts the certs:
volumes:
- ./nginx/ssl:/etc/nginx/ssl
@ -190,7 +288,7 @@ Configure NGINX to use the cert in production.
## 🧠 Development Best Practices
- Always run the DB via Docker
- Use `docker-compose.dev.yml` for development
- Use `docker-compose.dev.yaml` for development
- Never run PostgreSQL directly on host
- Run frontend and backend separately for hot reload
- Use `.env.example` as a template
@ -245,8 +343,8 @@ lsof -i :14000
Database Issues:
Ensure DB is in Docker and configured correctly
Try restarting:
docker-compose -f docker-compose.dev.yml down
docker-compose -f docker-compose.dev.yml up db
docker-compose -f docker-compose.dev.yaml down
docker-compose -f docker-compose.dev.yaml up db
CORS Issues:
Check API base URL in frontend `.env`
@ -350,16 +448,22 @@ To access your services running in Kubernetes from your local machine, use these
### Frontend (React app)
```bash
kubectl port-forward -n fusero svc/fusero-frontend-service 3000:80
kubectl port-forward -n fusero-dev svc/fusero-frontend-service 3000:80
```
- Access at: http://localhost:3000
### Backend (API)
```bash
kubectl port-forward -n fusero svc/fusero-backend-service 14000:14000
kubectl port-forward -n fusero-dev svc/fusero-backend-service 14000:14000
```
- Access at: http://localhost:14000
### Database
```bash
kubectl port-forward -n fusero-dev svc/postgres-service 5432:5432
```
- Access at: localhost:5432
---
## NGINX Backend Service Name: Docker Compose vs Kubernetes
@ -484,3 +588,252 @@ To connect to the Postgres database running in Kubernetes from your local machin
4. **Test the connection.**
---
## 🎯 Kubernetes Namespace Management
### Development Namespace Setup
```bash
# Create development namespace
kubectl create namespace fusero-dev
# Set current context to development namespace
kubectl config set-context --current --namespace=fusero-dev
# Deploy to development namespace
helm upgrade --install fusero ./chart -n fusero-dev -f chart/values.dev.yaml
```
### Production Namespace Setup
```bash
# Create production namespace
kubectl create namespace fusero-prod
# Set current context to production namespace
kubectl config set-context --current --namespace=fusero-prod
# Deploy to production namespace
helm upgrade --install fusero ./chart -n fusero-prod -f chart/values.prod.yaml
```
### Namespace Management Commands
```bash
# List all namespaces
kubectl get namespaces
# Switch between namespaces
kubectl config set-context --current --namespace=<namespace-name>
# View resources in current namespace
kubectl get all
# Delete namespace (be careful!)
kubectl delete namespace <namespace-name>
```
### Recommended Kubernetes GUI Tools
1. **Lens** - The most popular Kubernetes IDE
- Download: https://k8slens.dev/
- Features:
- Real-time cluster monitoring
- Multi-cluster management
- Namespace isolation
- Resource visualization
- Log streaming
- Terminal access
2. **K9s** - Terminal-based UI
- Install: `brew install k9s` (Mac) or `scoop install k9s` (Windows)
- Features:
- Fast navigation
- Resource management
- Log viewing
- Port forwarding
3. **Octant** - Web-based UI
- Install: https://octant.dev/
- Features:
- Resource visualization
- Configuration management
- Log viewing
- Port forwarding
---
## 🆕 Namespaced Development Environment with Helm
You have configured your Kubernetes development environment to use a dedicated namespace (`fusero-dev`) for all core services (backend, frontend, and database). This ensures resource isolation and easier management between development and production.
### What was changed:
- All Kubernetes service templates (`backend-service.yaml`, `frontend-service.yaml`, `postgres-service.yaml`) now use a namespace variable.
- The development values file (`chart/values.dev.yaml`) sets `global.namespace: fusero-dev` and updates all internal service hostnames to use this namespace.
- Helm deployments now target the `fusero-dev` namespace for all dev resources.
### How to use:
1. **Create the development namespace (if not already created):**
```bash
kubectl create namespace fusero-dev
```
2. **Deploy all services to the namespace:**
```bash
helm upgrade --install fusero ./chart -n fusero-dev -f chart/values.dev.yaml
```
3. **Check running pods and services:**
```bash
kubectl get all -n fusero-dev
```
4. **If you update environment variables or service hostnames, repeat the Helm upgrade command.**
5. **If you see errors about immutable fields (e.g., for Jobs), delete the old Job before redeploying:**
```bash
kubectl delete job <job-name> -n fusero-dev
helm upgrade --install fusero ./chart -n fusero-dev -f chart/values.dev.yaml
```
### Why use namespaces?
- Keeps dev and prod resources isolated
- Makes it easy to clean up or redeploy all dev resources
- Prevents accidental cross-environment access
---
## 🔒 Production Security & Best Practices
### Environment Variables & Secrets
The application uses a secure secrets management approach:
1. **Development Environment**:
- Use `.env` files locally (gitignored)
- Copy from `.env.example` as template
2. **Production Environment**:
- Secrets are managed through Gitea CI/CD secrets
- Template file: `chart/secrets.prod.template.yaml`
- Actual secrets are generated during deployment
- Never commit actual secrets to the repository
3. **Required Secrets**:
- Database credentials
- Admin user credentials
- Security keys (encryption, JWT)
- API keys (ChatGPT, Canvas)
4. **Secrets in CI/CD**:
- Secrets are stored in Gitea CI/CD settings
- Automatically injected during deployment
- Used to generate `secrets.prod.yaml` at runtime
5. **Security Best Practices**:
- All secrets files are gitignored
- Template files contain placeholder values
- Production secrets are never stored in the repository
- Regular rotation of secrets recommended
### HTTPS & Certificates
- In production, use trusted certificates (e.g., Let's Encrypt).
- Configure NGINX to enforce HTTPS.
### CORS & Security Headers
- Lock down CORS settings in production.
- Use security headers (e.g., HSTS, CSP).
### Logging & Monitoring
- Configure logging for production (e.g., ELK stack, Datadog).
- Set up basic monitoring (e.g., Prometheus, Grafana).
### Database Backup
- Regularly backup your production database.
- Example: Use `pg_dump` or a managed backup service.
### CI/CD & Automated Testing
- Run tests before deploying to production.
- Example CI/CD workflow:
```yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branch: main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Tests
run: npm test
- name: Deploy to Kubernetes
run: helm upgrade --install fusero ./chart -n fusero-prod -f chart/values.prod.yaml
```
### Troubleshooting Production
- Common issues:
- Database connection errors: Check secrets and network policies.
- Pod crashes: Check logs with `kubectl logs <pod-name> -n fusero-prod`.
- Rollback: Use `helm rollback fusero <revision> -n fusero-prod`.
---
## 🆕 Recent Improvements & Troubleshooting
- **Consistent File Extensions:** All Kubernetes and Helm YAML files now use the `.yaml` extension for consistency.
- **Secrets Management:**
- Development secrets are stored in `chart/secrets.dev.yaml` (gitignored).
- Production secrets are generated by CI/CD as `chart/secrets.prod.yaml` from Gitea secrets.
- All values files (`values.dev.yaml`, `values.prod.yaml`) reference secrets via environment variables.
- **CORS Configuration:**
- For local development, set `CORS_ORIGIN: "http://localhost:3000"` in `chart/values.dev.yaml` to allow credentialed requests from the frontend.
- Do **not** use `*` for CORS origin if you need credentials.
- **Kubernetes Job Immutability:**
- If you update environment variables or secrets, you must delete the old migration/seed job before redeploying:
```bash
kubectl delete job fusero-backend-db-init -n fusero-dev
helm upgrade --install fusero ./chart -n fusero-dev -f chart/values.dev.yaml -f chart/secrets.dev.yaml
```
- **Port Forwarding for Local Access:**
- Backend:
```bash
kubectl port-forward -n fusero-dev svc/fusero-backend-service 14000:14000
```
- Frontend:
```bash
kubectl port-forward -n fusero-dev svc/fusero-frontend-service 3000:80
```
- Database:
```bash
kubectl port-forward -n fusero-dev svc/postgres-service 5432:5432
```
- **Testing Login with curl:**
```bash
curl -i -X POST http://localhost:14000/api/v1/app/users/login \
-H "Content-Type: application/json" \
-H "Origin: http://localhost:3000" \
-d '{"username":"admin","password":"admin123"}'
```
- **Troubleshooting 401 Errors:**
- If login fails after a redeploy:
- Ensure secrets and values are in sync.
- Re-run the seed job as above.
- Check backend logs for authentication errors:
```bash
kubectl logs -n fusero-dev -l app=fusero-backend --tail=100
```
---
## 🚀 Production Deployment Pipeline (CI/CD)
- On every push/merge to `main`, the Gitea CI/CD pipeline will:
1. Build and push Docker images for backend and frontend.
2. Generate `secrets.prod.yaml` from Gitea CI/CD secrets.
3. **Delete the old migration/seed job** (`fusero-backend-db-init`) to ensure a fresh run.
4. Deploy the app with Helm, which triggers the migration/seed job.
5. **Wait for the migration/seed job to complete.**
6. **Fail the pipeline if the job fails** (with logs for debugging).
7. Verify the deployment.
- This ensures your database is always migrated and seeded with every deploy, and you'll know immediately if something goes wrong.
- To trigger a production deployment, just push or merge to `main`.

@ -1,6 +1,6 @@
apiVersion: v2
name: fusero
description: Fusero application Helm chart
description: Fusero App Boilerplate Helm Chart
type: application
version: 0.1.0
appVersion: "1.0.0"

6
chart/Chart.yml Normal file

@ -0,0 +1,6 @@
apiVersion: v2
name: fusero
description: Fusero App Boilerplate Helm Chart
type: application
version: 0.1.0
appVersion: "1.0.0"

@ -1,2 +1,16 @@
# You can skip this if you manage secrets separately
# Included for completeness (values-dev.yaml handles them)
apiVersion: v1
kind: Secret
metadata:
name: fusero-backend-secrets
namespace: {{ .Values.global.namespace }}
type: Opaque
data:
POSTGRES_PASSWORD: {{ .Values.backend.env.POSTGRES_PASSWORD | b64enc }}
DEFAULT_ADMIN_PASSWORD: {{ .Values.backend.env.DEFAULT_ADMIN_PASSWORD | b64enc }}
ENCRYPTION_KEY: {{ .Values.backend.env.ENCRYPTION_KEY | b64enc }}
JWT_SECRET: {{ .Values.backend.env.JWT_SECRET | b64enc }}
CHATGPT_API_KEY: {{ .Values.backend.env.CHATGPT_API_KEY | b64enc }}
CANVAS_API_KEY: {{ .Values.backend.env.CANVAS_API_KEY | b64enc }}

@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: fusero-backend-service
namespace: {{ .Values.global.namespace }}
spec:
selector:
app: fusero-backend

@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: fusero-frontend-service
namespace: {{ .Values.global.namespace }}
spec:
selector:
app: fusero-frontend

@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fusero-app-ingress
namespace: fusero-prod
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: app.fusero.nl
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: fusero-backend-service
port:
number: 14000
- path: /
pathType: Prefix
backend:
service:
name: fusero-frontend-service
port:
number: 80

@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
data:
pg_hba.conf: |
# TYPE DATABASE USER ADDRESS METHOD
local all all trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
host all all 0.0.0.0/0 md5
postgresql.conf: |
listen_addresses = '*'
max_connections = 100
shared_buffers = 128MB
dynamic_shared_memory_type = posix
max_wal_size = 1GB
min_wal_size = 80MB

@ -27,7 +27,16 @@ spec:
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-data
- mountPath: /etc/postgresql/pg_hba.conf
name: postgres-config
subPath: pg_hba.conf
- mountPath: /etc/postgresql/postgresql.conf
name: postgres-config
subPath: postgresql.conf
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc-fresh
- name: postgres-config
configMap:
name: postgres-config

@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: {{ .Values.global.namespace }}
spec:
selector:
app: postgres

@ -0,0 +1,53 @@
global:
namespace: fusero-prod
security:
cors:
origin: "https://your-domain.com"
methods: "GET,POST,PUT,DELETE"
credentials: true
https:
enabled: true
certSecret: "fusero-tls-secret"
logging:
level: "info"
format: "json"
monitoring:
prometheus:
enabled: true
path: "/metrics"
backend:
image: fusero-backend:latest
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
env:
# Non-sensitive values only
NODE_ENV: "production"
FASTIFY_PORT: "14000"
CANVAS_API_URL: "https://talnet.instructure.com/api/v1"
frontend:
image: fusero-frontend:latest
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "300m"
memory: "256Mi"
postgres:
image: postgres:15
storage: 5Gi
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"

130
docs/DEPLOY.md Normal file

@ -0,0 +1,130 @@
# 📦 Fusero VPS Deployment Guide
This guide walks you through deploying the Fusero full-stack app to a plain Ubuntu VPS using Kubernetes (k3s), Helm, and automatic HTTPS via cert-manager.
---
## 📋 Prerequisites
- ✅ Ubuntu 22.04 VPS with root or sudo access
- ✅ Domain names pointed to your VPS IP:
- api.fusero.nl → for the backend
- app.fusero.nl → for the frontend
- ✅ Git access to your repo
---
## ☸️ 1. Install Kubernetes (k3s)
curl -sfL https://get.k3s.io | sh -
Set kubeconfig so kubectl works:
echo 'export KUBECONFIG=/etc/rancher/k3s/k3s.yaml' >> ~/.bashrc
source ~/.bashrc
Verify:
kubectl get nodes
---
## 📦 2. Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Verify:
helm version
---
## 📁 3. Clone the Project
git clone https://your.gitea.repo/fusero-app-boilerplate.git
cd fusero-app-boilerplate
---
## 🔐 4. Set Up HTTPS (cert-manager)
Install cert-manager:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.3/cert-manager.yaml
Check pods:
kubectl get pods -n cert-manager
Create file cluster-issuer.yaml with this content:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your@email.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
Apply it:
kubectl apply -f cluster-issuer.yaml
---
## 🌍 5. Update DNS
Ensure both api.fusero.nl and app.fusero.nl point to your VPS IP address.
Check propagation:
ping api.fusero.nl
---
## 🚀 6. Deploy with Helm
Ensure you're in the repo root and the chart directory exists.
helm upgrade --install fusero ./chart -f chart/values-prod.yaml
This deploys frontend, backend, Postgres, ingress, and HTTPS.
---
## 📜 7. Verify Access
Frontend: https://app.fusero.nl
Backend API: https://api.fusero.nl
---
## 🔁 8. (Optional) Rerun DB Migrations
kubectl delete job fusero-backend-db-init
helm upgrade fusero ./chart -f chart/values-prod.yaml
---
## 🧪 9. Useful Commands
View backend logs:
kubectl logs deployment/fusero-backend
View frontend logs:
kubectl logs deployment/fusero-frontend
View pods and services:
kubectl get pods,svc,deployments
---
## ✅ Youre Done!
You now have a production deployment of Fusero on a raw VPS with:
- Kubernetes (via k3s)
- TLS via Let's Encrypt
- Helm-managed services
- DNS routing for subdomains
For CI/CD automation via Gitea, see `.gitea-ci.yml` in the repo root.

128
docs/GUIDE-TO-K8S.md Normal file

@ -0,0 +1,128 @@
# 📘 How to Install Kubernetes on Ubuntu 24.04 (Step-by-Step Guide)
This guide walks you through installing a multi-node Kubernetes cluster on Ubuntu 24.04 using `kubeadm`.
---
## 🧰 Prerequisites
* Ubuntu 24.04 instances with SSH enabled
* sudo user access
* At least 2GB RAM, 2 CPUs, and 20GB storage per node
* Internet access
### Sample Setup:
* **Master Node:** k8s-master-noble (192.168.1.120)
* **Worker 1:** k8s-worker01-noble (192.168.1.121)
* **Worker 2:** k8s-worker02-noble (192.168.1.122)
---
## 1⃣ Set Hostnames & Update Hosts File
Run on each node:
sudo hostnamectl set-hostname "k8s-master-noble" # Master
sudo hostnamectl set-hostname "k8s-worker01-noble" # Worker 1
sudo hostnamectl set-hostname "k8s-worker02-noble" # Worker 2
Edit `/etc/hosts` on all nodes:
192.168.1.120 k8s-master-noble
192.168.1.121 k8s-worker01-noble
192.168.1.122 k8s-worker02-noble
---
## 2⃣ Disable Swap & Load Kernel Modules
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
sudo modprobe overlay
sudo modprobe br\_netfilter
echo -e "overlay\nbr\_netfilter" | sudo tee /etc/modules-load.d/k8s.conf
echo -e "net.bridge.bridge-nf-call-ip6tables = 1\nnet.bridge.bridge-nf-call-iptables = 1\nnet.ipv4.ip\_forward = 1" | sudo tee /etc/sysctl.d/kubernetes.conf
sudo sysctl --system
---
## 3⃣ Install and Configure containerd
sudo apt install -y curl gnupg2 software-properties-common apt-transport-https ca-certificates
curl -fsSL [https://download.docker.com/linux/ubuntu/gpg](https://download.docker.com/linux/ubuntu/gpg) | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/containerd.gpg
sudo add-apt-repository "deb \[arch=amd64] [https://download.docker.com/linux/ubuntu](https://download.docker.com/linux/ubuntu) \$(lsb\_release -cs) stable"
sudo apt update && sudo apt install containerd.io -y
containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
---
## 4⃣ Add Kubernetes Repository
curl -fsSL [https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key](https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key) | sudo gpg --dearmor -o /etc/apt/keyrings/k8s.gpg
echo "deb \[signed-by=/etc/apt/keyrings/k8s.gpg] [https://pkgs.k8s.io/core:/stable:/v1.30/deb/](https://pkgs.k8s.io/core:/stable:/v1.30/deb/) /" | sudo tee /etc/apt/sources.list.d/k8s.list
---
## 5⃣ Install kubelet, kubeadm, kubectl
sudo apt update
sudo apt install kubelet kubeadm kubectl -y
---
## 6⃣ Initialize Kubernetes Cluster (Master Node Only)
sudo kubeadm init --control-plane-endpoint=k8s-master-noble
Then set up kubectl:
mkdir -p \$HOME/.kube
sudo cp /etc/kubernetes/admin.conf \$HOME/.kube/config
sudo chown \$(id -u):\$(id -g) \$HOME/.kube/config
---
## 7⃣ Join Worker Nodes
Use the join command from the `kubeadm init` output on each worker node:
sudo kubeadm join k8s-master-noble:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>
---
## 8⃣ Install Calico Network Add-on (Master Only)
kubectl apply -f [https://raw.githubusercontent.com/projectcalico/calico/v3.29.1/manifests/calico.yaml](https://raw.githubusercontent.com/projectcalico/calico/v3.29.1/manifests/calico.yaml)
Check readiness:
kubectl get pods -n kube-system
kubectl get nodes
---
## 9⃣ Test the Cluster
kubectl create ns demo-app
kubectl create deployment nginx-app --image nginx --replicas 2 --namespace demo-app
kubectl expose deployment nginx-app -n demo-app --type NodePort --port 80
kubectl get svc -n demo-app
Then access it:
curl http\://<worker-node-ip>:<node-port>
✅ You now have a fully functional Kubernetes cluster on Ubuntu 24.04!

@ -1,27 +0,0 @@
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Serve favicon files
location /favicon/ {
alias /usr/share/nginx/html/dist/favicon/;
access_log off;
expires max;
}
# Proxy API requests to the backend
location /api {
proxy_pass http://fusero-app-backend:14000;
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;
}
}

@ -1 +0,0 @@
/// <reference types="vite/client" />

@ -10,32 +10,32 @@
"@": "src"
},
"scripts": {
"fc": "fastify cli",
"build:ts": "rimraf dist && tsc",
"build:frontend:dev": "docker build -t fusero-frontend-dev:local -f frontend/Dockerfile.dev ./frontend",
"prebuild": "npm run lint",
"start": "npm run build:ts && fastify start -l info -r tsconfig-paths/register dist/src/app.js",
"dev": "npm run build:ts && concurrently -k -p '[{name}]' -n 'TypeScript,App,Watcher' -c 'teal.bold,orange.bold,purple.bold' 'npm:watch:ts' 'npm:dev:start' 'npm:watch:ts'",
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P -r tsconfig-paths/register dist/src/app.js",
"watch:ts": "tsc -w",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
"lint:fix": "eslint src/**/*.{js,jsx,ts,tsx} --fix",
"test": "jest",
"test:unit": "tap test/services/**/*.ts",
"test:integration": "tap test/integration/**/*.ts",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"start": "npm run build:ts && fastify start -l info -r tsconfig-paths/register dist/src/app.js",
"prebuild": "npm run lint",
"build:ts": "rimraf dist && tsc",
"build:frontend:dev": "docker build -t fusero-frontend-dev:local -f frontend/Dockerfile.dev ./frontend",
"k8s:dev:apply": "kubectl apply -f k8s/frontend/deployment.dev.yml",
"test:db": "ts-node -r tsconfig-paths/register src/database/test-connection.ts",
"migration:create": "npx mikro-orm migration:create",
"migrate": "npx mikro-orm migration:up",
"seed": "ts-node -r tsconfig-paths/register src/database/seeds/run-seed.ts",
"app:generate": "node ./utils/generate-app.js",
"k8s:dev:apply": "kubectl apply -f k8s/frontend/deployment.dev.yaml",
"k8s:dev:delete": "kubectl delete deployment fusero-frontend-dev 2>/dev/null || true",
"k8s:dev:run": "kubectl port-forward svc/fusero-frontend-service 3000:80",
"k8s:dev:describe": "kubectl describe pod -l app=fusero-frontend",
"k8s:dev:svc": "kubectl describe svc fusero-frontend-service",
"k8s:dev:deployment": "kubectl describe deployment fusero-frontend-dev",
"k8s:get": "kubectl get pods,svc,deployment",
"watch:ts": "tsc -w",
"dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"",
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P -r tsconfig-paths/register dist/src/app.js",
"app:generate": "node ./utils/generate-app.js",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}",
"lint:fix": "eslint src/**/*.{js,jsx,ts,tsx} --fix",
"migration:create": "npx mikro-orm migration:create",
"seed": "ts-node -r tsconfig-paths/register src/database/seeds/run-seed.ts",
"test:db": "ts-node -r tsconfig-paths/register src/database/test-connection.ts",
"k8s:exec": "kubectl exec -it $POD_NAME -- /bin/sh"
},
"keywords": [],
@ -103,4 +103,4 @@
"git add"
]
}
}
}

@ -129,6 +129,11 @@ const app: FastifyPluginAsync = async (app, opts): Promise<void> => {
export async function buildApp(): Promise<FastifyInstance> {
const server = fastify();
await server.register(app);
// Set the port to 14000
const port = process.env.PORT || 14000;
await server.listen({ port: Number(port), host: '0.0.0.0' });
return server;
}

@ -1,33 +0,0 @@
// import fp from 'fastify-plugin';
// import crypto from 'crypto';
// import { FastifyInstance, FastifyPluginAsync } from 'fastify';
// const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string; // Ensure this is 32 bytes for AES-256
// const IV_LENGTH = 16; // AES block size
// const cryptoPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
// fastify.decorate('encrypt', (text: string): string => {
// const iv = crypto.randomBytes(IV_LENGTH);
// const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
// let encrypted = cipher.update(text);
// encrypted = Buffer.concat([encrypted, cipher.final()]);
// return iv.toString('hex') + ':' + encrypted.toString('hex');
// });
// fastify.decorate('decrypt', (text: string): string => {
// const textParts = text.split(':');
// const iv = Buffer.from(textParts.shift()!, 'hex');
// const encryptedText = Buffer.from(textParts.join(':'), 'hex');
// const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
// let decrypted = decipher.update(encryptedText);
// decrypted = Buffer.concat([decrypted, decipher.final()]);
// return decrypted.toString();
// });
// };
// export default fp(cryptoPlugin, {
// name: 'cryptoPlugin',
// decorators: {
// fastify: ['encrypt', 'decrypt'],
// },
// });

@ -1,20 +0,0 @@
// import { FastifyPluginAsync } from 'fastify';
// import fJwt, { FastifyJWTOptions } from '@fastify/jwt';
// const jwtPlugin: FastifyPluginAsync<FastifyJWTOptions> = async (fastify) => {
// await fastify.register(fJwt, {
// secret: process.env.JWT_SECRET || 'your-secret-here',
// });
// };
// const authenticateDecorator: FastifyPluginAsync = async (fastify) => {
// fastify.decorate('authenticate', async (request, reply) => {
// try {
// await request.jwtVerify();
// } catch (err) {
// reply.send(err);
// }
// });
// };
// export { jwtPlugin, authenticateDecorator };

@ -1,15 +0,0 @@
// import { HttpStatusCode } from 'axios';
// class TooManyRequests extends Error {
// public httpErrorStatusCode = HttpStatusCode.TooManyRequests;
// constructor(msg: string, customErrorCode?: HttpStatusCode) {
// super(msg);
// this.httpErrorStatusCode = this.httpErrorStatusCode ?? customErrorCode;
// // Set the prototype explicitly.
// Object.setPrototypeOf(this, TooManyRequests.prototype);
// }
// }
// export default TooManyRequests;