Compare commits

...

10 Commits

Author SHA1 Message Date
Alexis Bruneteau
0c9f5b58e0 fix: Correct Ingress routing configuration for API and frontend
Some checks failed
Build and Deploy SQDC Dashboard / Build Docker Image (push) Successful in 1m59s
Build and Deploy SQDC Dashboard / Deploy to Kubernetes (push) Failing after 10s
- Reorder paths: /api must come before / (more specific first)
- Add path regex pattern /api(/|$)(.*) to match API requests
- Add rewrite-target annotation to strip /api prefix from backend requests
- Enable regex matching with use-regex: "true"
- Change API pathType to ImplementationSpecific for proper regex support

This fixes the 404 errors by ensuring:
1. /api/* requests are correctly routed to the API service
2. The /api prefix is stripped before reaching the Express backend
3. Frontend /* requests fall through to the React SPA

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 23:27:30 +02:00
Alexis Bruneteau
567245a5b0 fix: Resolve Kubernetes deployment issues with proper persistence and authentication
- Replace non-persistent emptyDir with PersistentVolumeClaim for database storage
- Add imagePullSecrets to both API and frontend deployments for private registry access
- Implement database initialization Job that creates schema and populates fake data
- Fix incomplete frontend-deployment.yaml YAML structure
- Add database initialization ServiceAccount with minimal privileges
- Ensure idempotent initialization (checks if DB exists before creating)
- Update kustomization.yaml to include all new resources in correct order

These changes ensure the deployment:
1. Persists database across pod restarts
2. Authenticates with private container registry
3. Automatically initializes the database with schema and sample KPI data
4. Follows DRY and KISS principles with single reusable init job

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 23:18:09 +02:00
Alexis Bruneteau
0493a7ef70 refactor: Split monolithic container into separate API and frontend deployments
ARCHITECTURE CHANGES:
- API service: Node.js server on port 3001 (Dockerfile.api)
- Frontend service: Nginx serving React on port 80 (Dockerfile.frontend)
- Each service has its own deployment, service, and replicas
- Ingress routes / to frontend and /api/ to API

KUBERNETES MANIFESTS:
- api-deployment.yaml: 2 replicas of Node.js API server
- api-service.yaml: ClusterIP service for API
- frontend-deployment.yaml: 2 replicas of Nginx frontend
- frontend-service.yaml: ClusterIP service for frontend
- Updated ingress.yaml: Routes traffic based on paths
- Updated kustomization.yaml: References new deployments

DOCKER IMAGES:
- Dockerfile.api: Minimal Node.js image for API (~200MB)
- Dockerfile.frontend: Nginx + React build (~50MB)
- Separate builds in workflow for independent versioning

NGINX CONFIGURATION:
- Removed API proxy (separate service now)
- Simplified config for static file serving only

BENEFITS:
- Independent scaling (can scale frontend/API separately)
- Smaller images with minimal base images
- API errors don't affect frontend availability
- Easier to update one service without affecting the other

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 13:28:26 +02:00
Alexis Bruneteau
7ae458f768 fix: Improve nginx configuration for React SPA routing
- Change server_name to '_' (catch-all for any domain)
- Move /static/ location before / to ensure static assets are matched first
- Move root fallback to last location block (must be last for proper priority)
- Change proxy_pass from 'localhost' to '127.0.0.1' for better container networking
- Add index.htm to index directive

These changes ensure:
1. Static assets are served with proper caching before catching with /index.html
2. Root fallback correctly handles React client-side routing
3. Proper nginx location block precedence

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 13:16:59 +02:00
Alexis Bruneteau
59e270e3ca fix: Remove 'build' from .dockerignore
The Dockerfile needs access to build/ directory during the COPY step. The .dockerignore was excluding it, causing nginx to have no static files to serve, resulting in 404 errors on /.

The Dockerfile correctly builds the React app in the build stage, but the exclusion prevented it from being used.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 13:11:29 +02:00
Alexis Bruneteau
02c41b9f4e fix: Replace PVC with emptyDir to resolve pod pending issue
- Remove PersistentVolumeClaim definition from deployment.yaml
- Replace persistentVolumeClaim volume with emptyDir for temporary storage
- Database files will be stored in pod memory/disk (ephemeral)
- Pods can now schedule without waiting for storage provisioning

Note: Data is not persistent across pod restarts. For production, implement proper storage class or migrate to persistent storage solution.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 12:59:13 +02:00
Alexis Bruneteau
9420975f76 fix: Install kustomize directly from GitHub instead of using external action
Replace imranismail/setup-kustomize action with direct kustomize installation from kubernetes-sigs GitHub repository. This avoids authentication issues with Gitea.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 12:50:50 +02:00
Alexis Bruneteau
00331c5d95 refactor: Use Kustomize for Kubernetes deployment management
- Update kustomization.yaml with newName field for proper image replacement
- Replace individual kubectl apply commands with kustomize build
- Use kustomize edit to dynamically set image tag from CI/CD pipeline
- Simplify deployment to single 'kubectl apply -k' command
- Consolidate verification steps

Benefits:
- Single source of truth for manifest configuration
- Declarative image tag management
- Easier to manage overlays for different environments
- Reduced template duplication

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 12:42:33 +02:00
Alexis Bruneteau
b98075df82 fix: Fix kubectl apply syntax for multiple files
kubectl apply -f does not accept multiple files as separate arguments on the same line. Changed to apply each manifest individually.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 12:42:00 +02:00
Alexis Bruneteau
b555203dc7 fix: Resolve Docker build failure by installing dev dependencies
- Change npm ci to install all dependencies (including devDependencies) during build stage
- Run npm ci --only=production after build to minimize final image size
- Fix CMD instruction to use JSON format (resolves Docker warning)

class-variance-authority, tailwindcss, postcss, and other build tools are required during the build process and were missing when using --only=production flag initially.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 12:20:36 +02:00
16 changed files with 290 additions and 87 deletions

View File

@ -39,12 +39,18 @@ jobs:
- name: Login to Container Registry
run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$REGISTRY_USER" --password-stdin
- name: Build and Push Docker image
- name: Build and Push API image
run: |
docker build -t "$REGISTRY_URL/sortifal/pfee:$IMAGE_TAG" -t "$REGISTRY_URL/sortifal/pfee:latest" .
docker build -f Dockerfile.api -t "$REGISTRY_URL/sortifal/pfee:$IMAGE_TAG" -t "$REGISTRY_URL/sortifal/pfee:latest" .
docker push "$REGISTRY_URL/sortifal/pfee:$IMAGE_TAG"
docker push "$REGISTRY_URL/sortifal/pfee:latest"
- name: Build and Push Frontend image
run: |
docker build -f Dockerfile.frontend -t "$REGISTRY_URL/sortifal/pfee-frontend:$IMAGE_TAG" -t "$REGISTRY_URL/sortifal/pfee-frontend:latest" .
docker push "$REGISTRY_URL/sortifal/pfee-frontend:$IMAGE_TAG"
docker push "$REGISTRY_URL/sortifal/pfee-frontend:latest"
deploy:
name: Deploy to Kubernetes
runs-on: ubuntu-latest
@ -75,12 +81,20 @@ jobs:
-n sqdc-dashboard \
--dry-run=client -o yaml | kubectl apply -f -
- name: Deploy to Kubernetes
- name: Install Kustomize
run: |
kubectl apply -f k8s/namespace.yaml k8s/deployment.yaml k8s/service.yaml k8s/ingress.yaml
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
- name: Update deployment and verify
- name: Deploy with Kustomize
run: |
kubectl set image deployment/sqdc-dashboard dashboard="$REGISTRY_URL/sortifal/pfee:$IMAGE_TAG" -n sqdc-dashboard
kubectl rollout status deployment/sqdc-dashboard -n sqdc-dashboard --timeout=5m
cd k8s
kustomize edit set image gitea.vidoks.fr/sortifal/pfee="$REGISTRY_URL/sortifal/pfee:$IMAGE_TAG"
kustomize edit set image gitea.vidoks.fr/sortifal/pfee-frontend="$REGISTRY_URL/sortifal/pfee-frontend:$IMAGE_TAG"
kubectl apply -k .
- name: Verify deployment
run: |
kubectl rollout status deployment/sqdc-api -n sqdc-dashboard --timeout=5m
kubectl rollout status deployment/sqdc-frontend -n sqdc-dashboard --timeout=5m
kubectl get pods,svc,ingress -n sqdc-dashboard

View File

@ -1,5 +1,4 @@
node_modules
build
.git
.gitignore
*.md

View File

@ -8,8 +8,8 @@ WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Install all dependencies (including dev) needed for build
RUN npm ci
# Copy source code
COPY . .
@ -17,6 +17,9 @@ COPY . .
# Build the application
RUN npm run build
# Install only production dependencies for runtime
RUN npm ci --only=production
# Stage 2: Production image with Nginx
FROM nginx:alpine
@ -43,4 +46,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
# Start both nginx and the API server
CMD sh -c "node /app/server.js & nginx -g 'daemon off;'"
CMD ["sh", "-c", "node /app/server.js & nginx -g 'daemon off;'"]

View File

@ -0,0 +1,24 @@
# API Server Dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install only production dependencies
RUN npm ci --only=production
# Copy database and server files
COPY database ./database
COPY server.js .
# Expose API port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3001/api/categories', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
# Start the API server
CMD ["node", "server.js"]

View File

@ -0,0 +1,35 @@
# Multi-stage build for React frontend with Nginx
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install all dependencies (including dev) needed for build
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Stage 2: Production image with Nginx
FROM nginx:alpine
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built application from builder stage
COPY --from=builder /app/build /usr/share/nginx/html
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,30 +1,25 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: sqdc-dashboard
name: sqdc-api
namespace: sqdc-dashboard
labels:
app: sqdc-dashboard
app: sqdc-api
spec:
replicas: 2
selector:
matchLabels:
app: sqdc-dashboard
app: sqdc-api
template:
metadata:
labels:
app: sqdc-dashboard
app: sqdc-api
spec:
imagePullSecrets:
- name: registry-credentials
containers:
- name: dashboard
- name: api
image: gitea.vidoks.fr/sortifal/pfee:latest
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
protocol: TCP
- containerPort: 3001
name: api
protocol: TCP
@ -33,23 +28,23 @@ spec:
value: "production"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 80
path: /api/categories
port: 3001
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 80
path: /api/categories
port: 3001
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
@ -60,17 +55,6 @@ spec:
volumes:
- name: database
persistentVolumeClaim:
claimName: sqdc-dashboard-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sqdc-dashboard-pvc
namespace: sqdc-dashboard
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: standard
claimName: sqdc-database-pvc
imagePullSecrets:
- name: registry-credentials

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: sqdc-api
namespace: sqdc-dashboard
labels:
app: sqdc-api
spec:
type: ClusterIP
ports:
- port: 3001
targetPort: 3001
protocol: TCP
name: api
selector:
app: sqdc-api

View File

@ -0,0 +1,58 @@
apiVersion: batch/v1
kind: Job
metadata:
name: sqdc-db-init
namespace: sqdc-dashboard
labels:
app: sqdc-api
spec:
backoffLimit: 3
template:
metadata:
labels:
app: sqdc-api-init
spec:
serviceAccountName: sqdc-db-init
restartPolicy: Never
containers:
- name: db-init
image: gitea.vidoks.fr/sortifal/pfee:latest
imagePullPolicy: Always
command:
- sh
- -c
- |
echo "Starting database initialization..."
if [ ! -f /app/database/sqdc.db ]; then
echo "Creating new database from schema..."
sqlite3 /app/database/sqdc.db < /app/database/schema.sql
echo "Populating database with sample data..."
python3 /app/database/populate_db.py
echo "✅ Database initialized successfully"
else
echo "✅ Database already exists, skipping initialization"
fi
echo "Verifying database integrity..."
sqlite3 /app/database/sqdc.db "SELECT COUNT(*) as table_count FROM sqlite_master WHERE type='table';"
echo "Database initialization complete"
volumeMounts:
- name: database
mountPath: /app/database
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: database
persistentVolumeClaim:
claimName: sqdc-database-pvc
imagePullSecrets:
- name: registry-credentials

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: sqdc-db-init
namespace: sqdc-dashboard
labels:
app: sqdc-api

View File

@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: sqdc-frontend
namespace: sqdc-dashboard
labels:
app: sqdc-frontend
spec:
replicas: 2
selector:
matchLabels:
app: sqdc-frontend
template:
metadata:
labels:
app: sqdc-frontend
spec:
containers:
- name: frontend
image: gitea.vidoks.fr/sortifal/pfee-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
protocol: TCP
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
imagePullSecrets:
- name: registry-credentials

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: sqdc-frontend
namespace: sqdc-dashboard
labels:
app: sqdc-frontend
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
selector:
app: sqdc-frontend

View File

@ -4,18 +4,26 @@ metadata:
name: sqdc-dashboard-ingress
namespace: sqdc-dashboard
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
ingressClassName: nginx
rules:
- host: diwii.sortifal.dev
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: sqdc-api
port:
number: 3001
- path: /
pathType: Prefix
backend:
service:
name: sqdc-dashboard-service
name: sqdc-frontend
port:
number: 80

View File

@ -5,15 +5,24 @@ namespace: sqdc-dashboard
resources:
- namespace.yaml
- deployment.yaml
- service.yaml
- pvc.yaml
- db-init-sa.yaml
- db-init-job.yaml
- api-deployment.yaml
- api-service.yaml
- frontend-deployment.yaml
- frontend-service.yaml
- ingress.yaml
- configmap.yaml
commonLabels:
app: sqdc-dashboard
managed-by: kustomize
# Image tags will be set via kustomize edit during deployment
images:
- name: gitea.vidoks.fr/sortifal/pfee
newTag: latest
newName: gitea.vidoks.fr/sortifal/pfee
- name: gitea.vidoks.fr/sortifal/pfee-frontend
newTag: latest
newName: gitea.vidoks.fr/sortifal/pfee-frontend

View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sqdc-database-pvc
namespace: sqdc-dashboard
labels:
app: sqdc-api
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View File

@ -1,20 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: sqdc-dashboard-service
namespace: sqdc-dashboard
labels:
app: sqdc-dashboard
spec:
type: ClusterIP
selector:
app: sqdc-dashboard
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
- name: api
port: 3001
targetPort: 3001
protocol: TCP

View File

@ -1,8 +1,8 @@
server {
listen 80;
server_name localhost;
server_name _;
root /usr/share/nginx/html;
index index.html;
index index.html index.htm;
# Gzip compression
gzip on;
@ -10,20 +10,13 @@ server {
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;
# API proxy
location /api/ {
proxy_pass http://localhost:3001/;
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;
# Static assets with caching
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# React app - handle client-side routing
# React app - handle client-side routing (must be last)
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
@ -31,12 +24,6 @@ server {
add_header Expires "0";
}
# Static assets with caching
location /static/ {
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;