From da2a95b56e529d469fd523eae61df6d77fd588cf Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sun, 4 Jan 2026 22:40:11 +0100 Subject: [PATCH] feat: Add combined Dockerfile for Swarm/Portainer deployment - Create single Dockerfile that builds frontend and backend together - Add nginx.conf for combined container (proxies to localhost) - Update docker-compose.prod.yml for Swarm compatibility: - Use pre-built image instead of build directive - Replace restart with deploy.restart_policy - Single 'app' service instead of separate backend/frontend --- Dockerfile | 72 +++++++++++++++++++++++++++++++++++++++++ docker-compose.prod.yml | 37 +++++---------------- nginx.conf | 66 +++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 Dockerfile create mode 100644 nginx.conf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..31ec614 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +# Build frontend +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Configure npm to use legacy peer deps +RUN npm config set legacy-peer-deps true + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +RUN npm run build + +# Build backend and final image +FROM node:20-alpine + +# Install MinIO client (detect architecture) +RUN ARCH=$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g') && \ + wget https://dl.min.io/client/mc/release/linux-${ARCH}/mc && \ + chmod +x mc && \ + mv mc /usr/local/bin/ + +# Install nginx for serving frontend +RUN apk add --no-cache nginx + +WORKDIR /app + +# Copy backend package files +COPY backend/package*.json ./ + +# Install build tools for native dependencies +RUN apk add --no-cache python3 make g++ + +# Install production dependencies +RUN npm ci --only=production + +# Rebuild bcrypt +RUN npm rebuild bcrypt --build-from-source + +# Copy backend application files +COPY backend/ ./ + +# Copy frontend build +COPY --from=frontend-builder /app/frontend/build /app/public + +# Copy nginx config for combined container +COPY nginx.conf /etc/nginx/http.d/default.conf + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Create necessary directories with proper permissions +RUN mkdir -p logs temp /tmp/minio-webui-temp /run/nginx && \ + chown -R nodejs:nodejs /app logs temp /tmp/minio-webui-temp && \ + chown -R nodejs:nodejs /var/lib/nginx /var/log/nginx /run/nginx + +# Create startup script +RUN echo '#!/bin/sh' > /app/start.sh && \ + echo 'nginx' >> /app/start.sh && \ + echo 'exec node src/app.js' >> /app/start.sh && \ + chmod +x /app/start.sh + +# Switch to non-root user +USER nodejs + +# Expose ports (3000 for API, 80 for frontend) +EXPOSE 3000 80 + +# Start both nginx and node +CMD ["/app/start.sh"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2d633d5..b5202eb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -7,20 +7,15 @@ services: command: redis-server --appendonly yes volumes: - /mnt/DockerMount/minio-webui/redis:/data - restart: unless-stopped networks: - minio-webui-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 3 + deploy: + restart_policy: + condition: unless-stopped - # Backend API - backend: - build: - context: ./backend - dockerfile: ../docker/Dockerfile.backend + # Combined app (backend + frontend) + app: + image: theluap/minio-webui:latest environment: - NODE_ENV=production - PORT=3000 @@ -53,25 +48,9 @@ services: - /mnt/DockerMount/minio-webui/mc-config:/home/nodejs/.mc depends_on: - redis - restart: unless-stopped - networks: - - minio-webui-network - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend (nginx + React) - frontend: - build: - context: ./frontend - dockerfile: ../docker/Dockerfile.frontend - depends_on: - - backend - restart: unless-stopped deploy: + restart_policy: + condition: unless-stopped labels: - "traefik.enable=true" - "traefik.http.routers.minio-webui.rule=Host(`minio-webui.local.nothaft.cloud`)" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..7970df4 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,66 @@ +server { + listen 80; + server_name localhost; + root /app/public; + 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 "no-referrer-when-downgrade" always; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # API proxy to local backend + location /api { + proxy_pass http://127.0.0.1:3000; + 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; + + # Timeouts for long-running operations + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # WebSocket support + location /ws { + proxy_pass http://127.0.0.1:3000; + 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; + } + + # Serve static files + location / { + try_files $uri $uri/ /index.html; + + # 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; + } +}