diff --git a/.env.example b/.env.example index 217d70c..a56f659 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX -CDN_DOMAIN=https://cdn.novalon.cn diff --git a/deploy.sh b/deploy.sh index b178e34..e1abd9e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -54,7 +54,7 @@ echo "" echo "📋 步骤0: 部署前检查..." -for file in docker-compose.yml Dockerfile nginx.conf .env.example setup-ssl.sh; do +for file in docker-compose.yml Dockerfile nginx-static.conf .env.example setup-ssl.sh; do if [ ! -f "$file" ]; then echo "❌ 缺少必要文件: $file" exit 1 @@ -85,7 +85,7 @@ echo "✅ SSH连接验证成功" echo "" echo "📋 步骤2: 上传部署文件..." ssh "$SERVER_USER@$SERVER_IP" "mkdir -p '$PROJECT_DIR'" -scp -r docker-compose.yml Dockerfile nginx.conf .env.example setup-ssl.sh "$SERVER_USER@$SERVER_IP:$PROJECT_DIR/" +scp -r docker-compose.yml Dockerfile nginx-static.conf .env.example setup-ssl.sh "$SERVER_USER@$SERVER_IP:$PROJECT_DIR/" echo "✅ 部署文件已上传" echo "" @@ -102,13 +102,12 @@ if [ ! -f .env ]; then echo "📝 创建.env文件..." cp .env.example .env echo "⚠️ 请编辑.env文件,填入正确的环境变量" - echo "⚠️ 必须配置: DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL, RESEND_API_KEY, OPS_ALERT_EMAIL" - exit 1 + echo "⚠️ 可选配置: NEXT_PUBLIC_GA_ID" fi echo "🐳 启动Docker容器..." docker-compose down -docker-compose pull +docker-compose build --no-cache docker-compose up -d echo "📋 等待服务启动..." @@ -118,7 +117,7 @@ check_interval=3 while [ $elapsed -lt $timeout ]; do if docker inspect --format='{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null | grep -q "running"; then - if curl -f -s -o /dev/null "http://localhost:3000" --max-time 5 2>/dev/null; then + if curl -f -s -o /dev/null "http://localhost:80" --max-time 5 2>/dev/null; then echo "✅ 服务已启动并响应正常" break else diff --git a/docker-compose-nginx.yml b/docker-compose-nginx.yml new file mode 100644 index 0000000..641fb6a --- /dev/null +++ b/docker-compose-nginx.yml @@ -0,0 +1,23 @@ +version: "3.8" + +services: + nginx: + image: nginx:alpine + container_name: novalon-nginx-secure + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx-static-production.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + - ./logs:/var/log/nginx + - ../certbot:/var/www/certbot + - ../novalon-static:/var/www/novalon:ro + networks: + - novalon-network + +networks: + novalon-network: + driver: bridge + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 41c476e..6af8677 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,9 @@ version: "3.8" services: novalon-website: + build: + context: . + dockerfile: Dockerfile image: novalon-website:1.0.0 container_name: novalon-website restart: unless-stopped diff --git a/nginx-static-production.conf b/nginx-static-production.conf new file mode 100644 index 0000000..831f1fb --- /dev/null +++ b/nginx-static-production.conf @@ -0,0 +1,237 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + resolver 127.0.0.11 valid=30s ipv6=off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + server_tokens off; + client_max_body_size 100M; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + limit_req_zone $binary_remote_addr zone=general:10m rate=100r/s; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+text text/javascript; + + upstream gitea_app { + server gitea:3000; + } + + upstream jenkins_app { + server jenkins:8080; + } + + # Novalon 主站 - 静态文件服务 + server { + listen 80; + server_name novalon.cn www.novalon.cn; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://www.novalon.cn$request_uri; + } + } + + server { + listen 443 ssl http2; + server_name novalon.cn www.novalon.cn; + + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # 静态文件根目录 + root /var/www/novalon; + index index.html; + + # 静态资源长期缓存 + location /_next/static/ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + try_files $uri =404; + } + + # 字体文件缓存 + location ~* \.(ttf|woff|woff2|eot)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Access-Control-Allow-Origin "*"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + try_files $uri =404; + } + + # 图片文件缓存 + location ~* \.(svg|jpg|jpeg|png|gif|webp|avif|ico)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + try_files $uri =404; + } + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Next.js 静态导出的页面路由 + location / { + limit_req zone=general burst=20 nodelay; + try_files $uri $uri.html $uri/ /404.html; + } + + # 自定义 404 页面 + error_page 404 /404.html; + + # 优化文件传输 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + } + + # Git 服务 + server { + listen 80; + server_name git.f.novalon.cn; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl http2; + server_name git.f.novalon.cn; + + ssl_certificate /etc/nginx/ssl/git.f.novalon.cn/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/git.f.novalon.cn/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + location / { + limit_req zone=general burst=20 nodelay; + proxy_pass http://gitea_app; + 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_hide_header X-Powered-By; + + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + } + + # CI 服务 + server { + listen 80; + server_name ci.f.novalon.cn; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl http2; + server_name ci.f.novalon.cn; + + ssl_certificate /etc/nginx/ssl/ci.f.novalon.cn/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/ci.f.novalon.cn/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + location / { + limit_req zone=general burst=20 nodelay; + proxy_pass http://jenkins_app; + 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_set_header X-Forwarded-Port $server_port; + + proxy_hide_header X-Powered-By; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + client_max_body_size 100m; + } + + access_log /var/log/nginx/jenkins-access.log; + error_log /var/log/nginx/jenkins-error.log; + } +} diff --git a/nginx-static.conf b/nginx-static.conf index f0aafc1..3378118 100644 --- a/nginx-static.conf +++ b/nginx-static.conf @@ -20,7 +20,7 @@ server { ssl_session_timeout 10m; # 静态文件根目录 - root /var/www/novalon; + root /usr/share/nginx/html; index index.html; # Gzip 压缩 @@ -79,7 +79,7 @@ server { # Let's Encrypt ACME challenge location /.well-known/acme-challenge/ { - root /var/www/certbot; + root /usr/share/nginx/html; try_files $uri =404; } diff --git a/public/logo.svg b/public/logo.svg index afc02b5..3c03301 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -2,7 +2,7 @@