updated dockerfile and improved url managing

This commit is contained in:
nvrl committed 2026-02-16 17:22:56 +01:00
1 parent d42f369d0f
commit 43c1c9d811
3 files changed
+57 -47

No files matched your search

+9 -10
View File
@@ -1,7 +1,7 @@
version: '3.8'
services:
# --- Backend Service ---
# --- Backend Service (Gunicorn + WhiteNoise) ---
backend:
build:
context: ./backend
@@ -20,37 +20,36 @@ services:
labels:
- "traefik.enable=true"
- "traefik.docker.network=narl"
# Explicitly set priority higher than the frontend to catch /api, /admin, etc.
# Higher priority for specific backend paths
- "traefik.http.routers.holy-backend.priority=20"
- "traefik.http.routers.holy-backend.rule=Host(`holy.narl.io`) && (PathPrefix(`/api`) || PathPrefix(`/admin`) || PathPrefix(`/static`) || PathPrefix(`/media`))"
- "traefik.http.routers.holy-backend.entrypoints=websecure"
- "traefik.http.routers.holy-backend.tls.certresolver=https_resolver"
- "traefik.http.services.holy-backend.loadbalancer.server.port=8000"
# --- Frontend Service ---
# --- Frontend Service (Nginx serving built React app) ---
frontend:
build:
context: ./frontend
args:
# Bake the API URL into the production build
- VITE_API_URL=https://holy.narl.io/api/
container_name: holy-frontend
restart: unless-stopped
networks:
- narl
env_file:
- ./.env
volumes:
- ./frontend:/app
- /app/node_modules
# In production, we don't mount volumes for code to ensure container integrity
depends_on:
- backend
labels:
- "traefik.enable=true"
- "traefik.docker.network=narl"
# Frontend acts as the fallback for anything else on this domain
# Lower priority acts as catch-all for the domain
- "traefik.http.routers.holy-frontend.priority=10"
- "traefik.http.routers.holy-frontend.rule=Host(`holy.narl.io`)"
- "traefik.http.routers.holy-frontend.entrypoints=websecure"
- "traefik.http.routers.holy-frontend.tls.certresolver=https_resolver"
- "traefik.http.services.holy-frontend.loadbalancer.server.port=5173"
- "traefik.http.services.holy-frontend.loadbalancer.server.port=80"
networks:
narl:
+24 -14
View File
@@ -1,20 +1,30 @@
# Use an official Node runtime as a parent image
FROM node:lts-alpine
# Set the working directory in the container
# --- Build Stage ---
FROM node:lts-alpine AS build-stage
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the frontend code
COPY . .
# Expose the port the app runs on
EXPOSE 5173
# Pass VITE environment variables during build
ARG VITE_API_URL
ENV VITE_API_URL=${VITE_API_URL}
# Start the development server
CMD ["npm", "run", "dev", "--", "--host"]
RUN npm run build
# --- Production Stage ---
FROM nginx:stable-alpine
# Copy built files
COPY --from=build-stage /app/dist /usr/share/nginx/html
# Custom Nginx config to handle React Router (SPA)
RUN printf 'server {\n\
listen 80;\n\
location / {\n\
root /usr/share/nginx/html;\n\
index index.html index.htm;\n\
try_files $uri $uri/ /index.html;\n\
}\n\
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+24 -23
View File
@@ -32,7 +32,7 @@ import ExpandMore from '@mui/icons-material/ExpandMore';
import { getTheme, type CatppuccinTheme } from './theme';
import api from './api';
// Code splitting for routes
// Lazy load components for production performance
const CategoryList = lazy(() => import('./pages/CategoryList'));
const CategoryFlavors = lazy(() => import('./pages/categories/CategoryFlavors'));
const FlavorDetail = lazy(() => import('./pages/flavors/FlavorDetail'));
@@ -58,7 +58,7 @@ const GlobalSearch = () => {
const fetchFlavors = async () => {
try {
const res = await api.get('flavors/');
setOptions(res.data);
setOptions(Array.isArray(res.data) ? res.data : (res.data.results || []));
} catch (err) {
console.error('Failed to fetch search options');
}
@@ -104,7 +104,7 @@ const GlobalSearch = () => {
<TextField
{...params}
fullWidth
placeholder="Search..."
placeholder="Search flavors..."
sx={{
bgcolor: 'action.hover',
borderRadius: 2,
@@ -155,16 +155,21 @@ const App: React.FC = () => {
useEffect(() => {
const fetchData = async () => {
try {
const token = localStorage.getItem('token');
const [userRes, catRes] = await Promise.all([
localStorage.getItem('token') ? api.get('users/me/') : Promise.resolve({ data: null }),
token ? api.get('users/me/') : Promise.resolve({ data: null }),
api.get('categories/')
]);
if (userRes.data) {
if (userRes.data && userRes.data.username) {
setUser(userRes.data);
if (userRes.data.theme) setThemeName(userRes.data.theme);
} else {
setUser(null);
}
setCategories(catRes.data);
const catData = Array.isArray(catRes.data) ? catRes.data : (catRes.data.results || []);
setCategories(catData);
} catch (err) {
setUser(null);
} finally {
@@ -199,7 +204,7 @@ const App: React.FC = () => {
<Box sx={{ width: 280 }} role="presentation">
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar src={user?.avatar || undefined} sx={{ width: 40, height: 40, border: '2px solid', borderColor: 'primary.main' }}>
{user && !user.avatar && user.username.charAt(0).toUpperCase()}
{user?.username && !user.avatar && user.username.charAt(0).toUpperCase()}
</Avatar>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
<Link to="/" onClick={() => setDrawerOpen(false)} style={{ color: 'inherit', textDecoration: 'none' }}>Holy Flavors</Link>
@@ -277,7 +282,6 @@ const App: React.FC = () => {
zIndex: (theme) => theme.zIndex.drawer + 1
}}>
<Toolbar sx={{ minHeight: { xs: 56, sm: 64 }, px: { xs: 1, sm: 4, md: 6 } }}>
{/* Brand Logo - Responsive */}
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
<Link to="/" style={{ color: 'inherit', textDecoration: 'none' }}>
<Box component="span" sx={{ display: { xs: 'none', sm: 'inline' } }}>Holy Flavors</Box>
@@ -285,7 +289,6 @@ const App: React.FC = () => {
</Link>
</Typography>
{/* Categories Dropdown - Desktop Only */}
<Box sx={{ display: { xs: 'none', md: 'flex' }, ml: 4 }}>
<Button
color="inherit"
@@ -315,18 +318,15 @@ const App: React.FC = () => {
</Menu>
</Box>
{/* Global Search - All Devices */}
<GlobalSearch />
{/* Right Side Icons */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/* User Menu - Desktop Only */}
{!loadingUser && (
user ? (
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
<Avatar src={user.avatar || undefined} sx={{ width: 36, height: 36, border: '2px solid', borderColor: 'primary.main' }}>
{!user.avatar && user.username.charAt(0).toUpperCase()}
{user.username && !user.avatar && user.username.charAt(0).toUpperCase()}
</Avatar>
</IconButton>
<Menu
@@ -349,7 +349,6 @@ const App: React.FC = () => {
)
)}
{/* Hamburger Menu - Mobile Only */}
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
<IconButton
color="inherit"
@@ -375,15 +374,17 @@ const App: React.FC = () => {
<Box sx={{ flexGrow: 1, width: '100%' }}>
<Suspense fallback={<Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>}>
<Routes>
<Route path="/" element={<CategoryList />} />
<Route path="/category/:slug" element={<CategoryFlavors />} />
<Route path="/flavor/:id" element={<FlavorDetail />} />
<Route path="/profile/:username" element={<PublicProfile />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
<Route path="/login" element={<Login />} />
</Routes>
<Routes>
<Route path="/" element={<CategoryList />} />
<Route path="/category/:slug" element={<CategoryFlavors />} />
<Route path="/flavor/:id" element={<FlavorDetail />} />
<Route path="/profile/:username" element={<PublicProfile />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
<Route path="/login" element={<Login />} />
{/* Fallback route for unknown paths */}
<Route path="*" element={<Box sx={{ p: 4, textAlign: 'center' }}><Typography variant="h5">404 - Page Not Found</Typography><Button component={Link} to="/" sx={{ mt: 2 }}>Back to Home</Button></Box>} />
</Routes>
</Suspense>
</Box>
</Box>