Add base kubernetes support

This commit is contained in:
Urtzi Alfaro
2025-09-27 11:18:13 +02:00
parent a27f159e24
commit 63a3f9c77a
63 changed files with 5826 additions and 170 deletions

71
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,71 @@
# Production Dockerfile for Frontend with Nginx
# Multi-stage build for optimal size and performance
# Stage 1: Build the application
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies including dev dependencies for building
RUN npm ci --verbose && \
npm cache clean --force
# Copy source code
COPY . .
# Build the application for production
RUN npm run build
# Stage 2: Production server with Nginx
FROM nginx:1.25-alpine AS production
# Install curl for health checks
RUN apk add --no-cache curl
# Remove default nginx configuration
RUN rm /etc/nginx/conf.d/default.conf
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/
# Copy built application from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Nginx user already exists in the base image, just ensure proper ownership
# Set proper permissions
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d
# Create nginx PID directory and fix permissions
RUN mkdir -p /var/run/nginx /var/lib/nginx/tmp && \
chown -R nginx:nginx /var/run/nginx /var/lib/nginx
# Custom nginx.conf for running as non-root
RUN echo 'pid /var/run/nginx/nginx.pid;' > /etc/nginx/nginx.conf && \
echo 'events { worker_connections 1024; }' >> /etc/nginx/nginx.conf && \
echo 'http {' >> /etc/nginx/nginx.conf && \
echo ' include /etc/nginx/mime.types;' >> /etc/nginx/nginx.conf && \
echo ' default_type application/octet-stream;' >> /etc/nginx/nginx.conf && \
echo ' sendfile on;' >> /etc/nginx/nginx.conf && \
echo ' keepalive_timeout 65;' >> /etc/nginx/nginx.conf && \
echo ' include /etc/nginx/conf.d/*.conf;' >> /etc/nginx/nginx.conf && \
echo '}' >> /etc/nginx/nginx.conf
# Switch to non-root user
USER nginx
# Expose port 3000 (to match current setup)
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:3000/ || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,13 +1,20 @@
# frontend/nginx.conf
events {
worker_connections 1024;
}
# Nginx configuration for Bakery IA Frontend
# This file is used inside the container at /etc/nginx/conf.d/default.conf
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Enable gzip compression
server {
listen 3000;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 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 "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com https://js.stripe.com; script-src-elem 'self' 'unsafe-inline' https://js.stripe.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' http://localhost:8000 http://localhost:8006 ws: wss:; frame-src https://js.stripe.com;" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
@@ -24,47 +31,83 @@ http {
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# API proxy to gateway service (Kubernetes internal name)
location /api/ {
proxy_pass http://gateway:8000;
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;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
# 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 "strict-origin-when-cross-origin" always;
# CORS headers for API requests
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
# Handle React Router
location / {
try_files $uri $uri/ /index.html;
}
# API proxy to backend
location /api/ {
proxy_pass http://bakery-gateway:8000/api/;
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;
proxy_cache_bypass $http_upgrade;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
}
# Static assets with aggressive caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary Accept-Encoding;
access_log off;
try_files $uri @fallback;
}
# Special handling for PWA assets
location ~* \.(webmanifest|manifest\.json)$ {
expires 1d;
add_header Cache-Control "public";
add_header Content-Type application/manifest+json;
}
location = /sw.js {
expires 1d;
add_header Cache-Control "public";
add_header Content-Type application/javascript;
}
# Main location block for SPA routing
location / {
try_files $uri $uri/ @fallback;
}
# Fallback for SPA routing - serve index.html
location @fallback {
rewrite ^.*$ /index.html last;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><text y="14" font-size="14">🍞</text></svg>

After

Width:  |  Height:  |  Size: 106 B

View File

@@ -3,8 +3,8 @@ const urlsToCache = [
'/',
'/index.html',
'/manifest.json',
'/icons/icon-192.png',
'/icons/icon-512.png',
'/manifest.webmanifest',
'/favicon.ico',
];
// Install event - cache assets
@@ -58,12 +58,28 @@ self.addEventListener('fetch', (event) => {
return;
}
// Static assets - cache first, network fallback
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
// Static assets - network first, cache fallback (for versioned assets)
if (event.request.destination === 'script' || event.request.destination === 'style' || event.request.destination === 'image') {
event.respondWith(
fetch(event.request).then((response) => {
// Clone the response before caching
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
}).catch(() => {
return caches.match(event.request);
})
);
} else {
// Other requests - cache first, network fallback
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
}
});
// Background sync for offline actions
@@ -120,4 +136,4 @@ async function syncInventoryData() {
} catch (error) {
console.error('Sync failed:', error);
}
}
}

View File

@@ -7,19 +7,7 @@ import './styles/animations.css';
import './styles/themes/light.css';
import './styles/themes/dark.css';
// Register service worker for PWA
if ('serviceWorker' in navigator && import.meta.env.VITE_ENABLE_PWA === 'true') {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(
(registration) => {
console.log('SW registered:', registration);
},
(error) => {
console.log('SW registration failed:', error);
}
);
});
}
// PWA/ServiceWorker functionality removed to avoid conflicts in development
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>

View File

@@ -1,52 +1,12 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
import path from 'path';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'prompt',
injectRegister: false, // Disable auto-registration
includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
manifest: {
name: 'Bakery AI',
short_name: 'Bakery AI',
theme_color: '#f97316',
icons: [
{
src: '/icons/icon-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
},
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.bakeryai\.com\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
}),
// PWA plugin temporarily disabled to avoid service worker conflicts
// VitePWA can be re-enabled later for production PWA features
],
resolve: {
alias: {
@@ -68,7 +28,9 @@ export default defineConfig({
},
proxy: {
'/api': {
target: 'http://localhost:8000',
target: process.env.NODE_ENV === 'development'
? 'http://gateway:8000' // Use internal service name in Kubernetes
: 'http://localhost:8000',
changeOrigin: true,
},
},