diff --git a/.gitignore b/.gitignore index 15e47bd..858d77c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ dist-ssr .env.development.local .env.test.local .env.production.local +.env.mysql +.mysql-credentials +backend/.env # PWA files sw.js diff --git a/Dockerfile b/Dockerfile index f4f570b..b3d67c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,30 +3,28 @@ FROM node:20-alpine AS builder WORKDIR /app -# Copy package files +# Build-time proxy (host SOCKS on 127.0.0.1:1081 — requires docker build --network=host) +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG ALL_PROXY +ENV HTTP_PROXY=$HTTP_PROXY \ + HTTPS_PROXY=$HTTPS_PROXY \ + ALL_PROXY=$ALL_PROXY \ + NODE_OPTIONS=--dns-result-order=ipv4first + COPY package*.json ./ -# Install dependencies RUN npm ci -# Copy source code COPY . . - -# Build the application RUN npm run build # Production stage FROM nginx:alpine -# Copy built files from builder stage COPY --from=builder /app/dist /usr/share/nginx/html - -# Copy nginx configuration COPY nginx.conf /etc/nginx/conf.d/default.conf -# Expose port 80 EXPOSE 80 -# Start nginx CMD ["nginx", "-g", "daemon off;"] - diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ec14a55 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,53 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +# Build-time proxy (host SOCKS on 127.0.0.1:1081 — requires docker build --network=host) +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG ALL_PROXY +ENV HTTP_PROXY=$HTTP_PROXY \ + HTTPS_PROXY=$HTTPS_PROXY \ + ALL_PROXY=$ALL_PROXY \ + NODE_OPTIONS=--dns-result-order=ipv4first + +COPY package*.json ./ +COPY prisma ./prisma/ + +RUN npm ci + +COPY . . +RUN npm run build + +FROM node:20-alpine + +WORKDIR /app + +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG ALL_PROXY +ENV HTTP_PROXY=$HTTP_PROXY \ + HTTPS_PROXY=$HTTPS_PROXY \ + ALL_PROXY=$ALL_PROXY \ + NODE_OPTIONS=--dns-result-order=ipv4first + +RUN apk add --no-cache openssl + +COPY package*.json ./ +COPY prisma ./prisma/ + +# Keep devDependencies (tsx) so `prisma db seed` works on container start +RUN npm ci + +# Do not keep proxy env in the final runtime image +ENV HTTP_PROXY= \ + HTTPS_PROXY= \ + ALL_PROXY= + +COPY --from=builder /app/dist ./dist +# Seed runs via tsx and imports application modules from src/ +COPY --from=builder /app/src ./src + +EXPOSE 3001 + +CMD ["sh", "-c", "npx prisma migrate deploy && npx prisma db seed && node dist/server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 54cd406..1477a20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,67 @@ -version: '3.8' - services: + mysql: + # Host CPU lacks x86-64-v2 (no SSE4.2); MySQL 8.x official images crash with glibc fatal. + image: mysql:5.7 + container_name: badi-pwa-mysql + restart: unless-stopped + env_file: + - .env.mysql + # Do NOT publish 3306 — public MySQL is a common ransomware target (RECOVER_YOUR_DATA). + # Backend reaches MySQL via the internal Docker network only. + volumes: + - badi_pwa_mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD || exit 1"] + interval: 5s + timeout: 5s + retries: 20 + + backend: + build: + context: ./backend + dockerfile: Dockerfile + network: host + args: + # HTTP forwarder (gost) → host SOCKS 127.0.0.1:1081 — Prisma needs HTTP proxy + HTTP_PROXY: http://127.0.0.1:8118 + HTTPS_PROXY: http://127.0.0.1:8118 + ALL_PROXY: http://127.0.0.1:8118 + image: badi-pwa-backend:latest + container_name: badi-pwa-backend + ports: + - "3001:3001" + restart: unless-stopped + env_file: + - ./backend/.env + environment: + - NODE_ENV=production + - PORT=3001 + - HOST=0.0.0.0 + # DATABASE_URL comes from backend/.env (do not hardcode credentials here) + - CORS_ORIGIN=https://pwa.feep.ir,https://novya.ir + - TRUST_PROXY=true + depends_on: + mysql: + condition: service_healthy + app: build: context: . dockerfile: Dockerfile - container_name: pwa-react-app + network: host + args: + HTTP_PROXY: http://127.0.0.1:8118 + HTTPS_PROXY: http://127.0.0.1:8118 + ALL_PROXY: http://127.0.0.1:8118 + image: badi-pwa-app:latest + container_name: badi-pwa-app ports: - - "3000:80" + - "3020:80" restart: unless-stopped environment: - NODE_ENV=production - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health", "||", "exit", "1"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + depends_on: + - backend +volumes: + badi_pwa_mysql_data: diff --git a/nginx.conf b/nginx.conf index 4613433..3500080 100644 --- a/nginx.conf +++ b/nginx.conf @@ -4,41 +4,58 @@ server { root /usr/share/nginx/html; index index.html; - # Gzip compression gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; - # 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; - # Handle client-side routing (SPA) + # Re-resolve Docker DNS so backend IP changes after recreate don't stick as 502 + resolver 127.0.0.11 valid=10s ipv6=off; + + # Frontend → /api/backend/* → Hono. Variable upstream + rewrite avoids stale DNS + # and still strips the /api/backend prefix. + location /api/backend/ { + rewrite ^/api/backend/(.*)$ /$1 break; + set $backend_upstream backend:3001; + proxy_pass http://$backend_upstream; + 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_connect_timeout 5s; + proxy_read_timeout 180s; + proxy_send_timeout 180s; + } + + location = /api/backend { + return 301 /api/backend/; + } + 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"; } - # Don't cache HTML files location ~* \.html$ { expires -1; add_header Cache-Control "no-store, no-cache, must-revalidate"; } - # Health check endpoint location /health { access_log off; return 200 "healthy\n"; add_header Content-Type text/plain; } } - - -