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>
This commit is contained in:
Alexis Bruneteau 2025-10-22 13:28:26 +02:00
parent 7ae458f768
commit 0493a7ef70
11 changed files with 178 additions and 60 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
@ -84,9 +90,11 @@ jobs:
run: |
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-dashboard -n sqdc-dashboard --timeout=5m
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

@ -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

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,48 @@
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

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,7 +4,6 @@ metadata:
name: sqdc-dashboard-ingress
namespace: sqdc-dashboard
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx
@ -16,6 +15,13 @@ spec:
pathType: Prefix
backend:
service:
name: sqdc-dashboard-service
name: sqdc-frontend
port:
number: 80
- path: /api/
pathType: Prefix
backend:
service:
name: sqdc-api
port:
number: 3001

View File

@ -5,18 +5,21 @@ namespace: sqdc-dashboard
resources:
- namespace.yaml
- deployment.yaml
- service.yaml
- api-deployment.yaml
- api-service.yaml
- frontend-deployment.yaml
- frontend-service.yaml
- ingress.yaml
- configmap.yaml
# Note: PVC removed - using emptyDir for temporary storage
commonLabels:
app: sqdc-dashboard
managed-by: kustomize
# Image tag will be set via kustomize edit or --kustomize-replace during deployment
# 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

@ -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

@ -10,19 +10,6 @@ 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://127.0.0.1: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;