Add base kubernetes support
This commit is contained in:
71
frontend/Dockerfile
Normal file
71
frontend/Dockerfile
Normal 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;"]
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
1
frontend/public/favicon.ico
Normal file
1
frontend/public/favicon.ico
Normal 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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user