diff --git a/Dockerfile b/Dockerfile index c385a13..3cd1198 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,6 @@ apt-get install -y --no-install-recommends \ curl \ libjpeg62-turbo \ libpq5 \ - nginx \ openssl \ passwd \ supervisor \ @@ -35,6 +34,9 @@ pip install -r /app/deploy/requirements.txt rm -rf /var/lib/apt/lists/* EOS +# Caddy 官方镜像里是静态链接的 Go 二进制,直接拷进 slim 就能跑,不需要额外依赖。 +COPY --from=caddy:2-alpine /usr/bin/caddy /usr/bin/caddy + COPY --chmod=755 ./ /app/ RUN mkdir -p /app/dist/ diff --git a/deploy/caddy/Caddyfile b/deploy/caddy/Caddyfile new file mode 100644 index 0000000..d5dac37 --- /dev/null +++ b/deploy/caddy/Caddyfile @@ -0,0 +1,77 @@ +{ + admin off + + servers { + # TLS 由前面的 Nginx Proxy Manager 终止,转发进来是明文 http。 + # 只信任内网来源的 X-Forwarded-For,{client_ip} 才解析得出学生的真实 IP。 + trusted_proxies static private_ranges + } +} + +# 站点地址只写端口,Caddy 就不会去申请证书(auto-HTTPS 自动关闭)。 +:8000 { + encode gzip + + # 和 NPM 那一层的 client_max_body_size 保持一致,上传测试用例压缩包用得上。 + # 必须写 MiB:Caddy 的 MB 按 10^6 算,200MB 比 nginx 的 200M 小了将近 10M。 + request_body { + max_size 200MiB + } + + # Caddy 的 header 会跨层叠加,不像 nginx 的 add_header 一旦在子块出现就不再继承, + # 所以这里写一次就够,下面各 handle 里只管自己的 Cache-Control。 + header { + X-XSS-Protection "1; mode=block" + X-Frame-Options SAMEORIGIN + X-Content-Type-Options nosniff + } + + log { + output file /data/log/caddy_access.log { + roll_size 10MiB + roll_keep 10 + } + } + + # 判题机心跳每秒一次;静态资源量大且带永久缓存。两者都不记访问日志。 + @nolog path /api/judge_server_heartbeat/ /assets/* + log_skip @nolog + + handle /api/* { + reverse_proxy 127.0.0.1:8080 { + header_up X-Real-IP {client_ip} + # AI 分析走 SSE。X-Accel-Buffering 是 nginx 专有的,Caddy 不认, + # 这里显式关掉响应缓冲,保证流式输出逐块下发。 + flush_interval -1 + } + } + + handle /ws/* { + reverse_proxy 127.0.0.1:8080 { + header_up X-Real-IP {client_ip} + } + } + + # 头像、上传文件、站点图标,实际落在 /data/public 下。 + handle /public/* { + root * /data + file_server + } + + # 构建产物文件名带内容 hash,内容一变文件名就变,可以永久缓存。 + # 命中后浏览器直接读磁盘,不再发条件请求。 + handle /assets/* { + root * /app/dist + header Cache-Control "public, max-age=31536000, immutable" + file_server + } + + # index.html 引用着带 hash 的文件名,必须每次回源校验, + # 否则发新版后学生刷不到。no-cache 是"缓存但每次校验",命中走 304。 + handle { + root * /app/dist + header Cache-Control "no-cache" + try_files {path} /index.html + file_server + } +} diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh index 3e7c5c8..a484212 100755 --- a/deploy/entrypoint.sh +++ b/deploy/entrypoint.sh @@ -3,7 +3,7 @@ APP=/app DATA=/data -mkdir -p "$DATA/log" "$DATA/config" "$DATA/ssl" "$DATA/test_case" "$DATA/public/upload" "$DATA/public/avatar" "$DATA/public/website" +mkdir -p "$DATA/log" "$DATA/config" "$DATA/test_case" "$DATA/public/upload" "$DATA/public/avatar" "$DATA/public/website" if [ ! -f "$DATA/config/secret.key" ]; then echo "$(head -c 32 /dev/urandom | md5sum | head -c 32)" > "$DATA/config/secret.key" @@ -17,26 +17,6 @@ if [ ! -f "$DATA/public/website/favicon.ico" ]; then cp data/public/website/favicon.ico "$DATA/public/website" fi -SSL="$DATA/ssl" -if [ ! -f "$SSL/server.key" ]; then - openssl req -x509 -newkey rsa:2048 -keyout "$SSL/server.key" -out "$SSL/server.crt" -days 1000 \ - -subj "/C=CN/ST=Beijing/L=Beijing/O=Beijing OnlineJudge Technology Co., Ltd./OU=Service Infrastructure Department/CN=$(hostname)" -nodes -fi - -cd "$APP/deploy/nginx" -ln -sf locations.conf https_locations.conf -if [ -z "$FORCE_HTTPS" ]; then - ln -sf locations.conf http_locations.conf -else - ln -sf https_redirect.conf http_locations.conf -fi - -if [ -n "$LOWER_IP_HEADER" ]; then - sed -i "s/__IP_HEADER__/\$http_$LOWER_IP_HEADER/g" api_proxy.conf; -else - sed -i "s/__IP_HEADER__/\$remote_addr/g" api_proxy.conf; -fi - if [ -z "$MAX_WORKER_NUM" ]; then CPU_CORE_NUM=$(grep -c ^processor /proc/cpuinfo) export CPU_CORE_NUM diff --git a/deploy/nginx/api_proxy.conf b/deploy/nginx/api_proxy.conf deleted file mode 100644 index cc508c8..0000000 --- a/deploy/nginx/api_proxy.conf +++ /dev/null @@ -1,6 +0,0 @@ -proxy_pass http://backend; -proxy_set_header X-Real-IP __IP_HEADER__; -proxy_set_header Host $http_host; -client_max_body_size 200M; -proxy_http_version 1.1; -proxy_set_header Connection ''; \ No newline at end of file diff --git a/deploy/nginx/https_redirect.conf b/deploy/nginx/https_redirect.conf deleted file mode 100644 index 88d8103..0000000 --- a/deploy/nginx/https_redirect.conf +++ /dev/null @@ -1,7 +0,0 @@ -location /api/judge_server_heartbeat { - include api_proxy.conf; -} - -location / { - return 301 https://$host$request_uri; -} \ No newline at end of file diff --git a/deploy/nginx/locations.conf b/deploy/nginx/locations.conf deleted file mode 100644 index 17d0018..0000000 --- a/deploy/nginx/locations.conf +++ /dev/null @@ -1,59 +0,0 @@ -location /public { - root /data; -} - -# WebSocket 支持 -location /ws/ { - proxy_pass http://backend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP __IP_HEADER__; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket 超时设置 - proxy_connect_timeout 7d; - proxy_send_timeout 7d; - proxy_read_timeout 7d; -} - -location /api { - include api_proxy.conf; -} - -# location /admin { -# root /app/dist/admin; -# try_files $uri $uri/ /index.html =404; -# } - -location /.well-known { - alias /data/ssl/.well-known; -} - -# 注意:location 里一旦出现 add_header,http 级的 add_header 就不再继承, -# 所以下面两个 block 都要把 nginx.conf 里的安全头重新写一遍。 - -# 构建产物文件名带内容 hash,内容一变文件名就变,可以永久缓存。 -# 命中后浏览器直接读磁盘,不再发条件请求。 -location /assets/ { - root /app/dist; - expires 1y; - add_header Cache-Control "public, immutable"; - add_header X-XSS-Protection "1; mode=block" always; - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; - access_log off; -} - -location / { - root /app/dist; - try_files $uri $uri/ /index.html =404; - # index.html 引用着带 hash 的文件名,必须每次回源校验, - # 否则发新版后学生刷不到。no-cache 是"缓存但每次校验",命中走 304。 - add_header Cache-Control "no-cache"; - add_header X-XSS-Protection "1; mode=block" always; - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; -} diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf deleted file mode 100644 index 839a585..0000000 --- a/deploy/nginx/nginx.conf +++ /dev/null @@ -1,76 +0,0 @@ -user www-data; -daemon off; -pid /tmp/nginx.pid; -worker_processes auto; -pcre_jit on; -error_log /data/log/nginx_error.log warn; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - server_tokens off; - keepalive_timeout 65; - sendfile on; - tcp_nodelay on; - - gzip on; - gzip_vary on; - gzip_comp_level 6; - gzip_min_length 1024; - # nginx 1.21.1 起 mime.types 把 .js 标成 text/javascript, - # 只写 application/javascript 会导致前端 JS 完全不压缩,两个都要留。 - # text/html 是内置的,不用也不能写在这里。 - gzip_types - text/javascript - application/javascript - text/css - application/json - image/svg+xml - text/plain; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - map $request $loggable { - default 1; - "~*^POST /api/judge_server_heartbeat/ HTTP/1\.1$" 0; - } - - access_log /data/log/nginx_access.log main if=$loggable; - # access_log off; - - upstream backend { - server 127.0.0.1:8080; - keepalive 32; - } - -add_header X-XSS-Protection "1; mode=block" always; - add_header X-Frame-Options SAMEORIGIN always; - add_header X-Content-Type-Options nosniff always; - - server { - listen 8000 default_server; - server_name _; - - include locations.conf; - } - - # server { - # listen 1443 ssl http2 default_server; - # server_name _; - # ssl_certificate /data/ssl/server.crt; - # ssl_certificate_key /data/ssl/server.key; - # ssl_protocols TLSv1.2; - # ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; - # ssl_prefer_server_ciphers on; - # ssl_session_cache shared:SSL:10m; - - # include https_locations.conf; - # } - -} diff --git a/deploy/supervisord.conf b/deploy/supervisord.conf index ee155b4..68edc55 100644 --- a/deploy/supervisord.conf +++ b/deploy/supervisord.conf @@ -16,11 +16,14 @@ supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=http://127.0.0.1:9005 -[program:nginx] -command=nginx -c /app/deploy/nginx/nginx.conf +[program:caddy] +command=caddy run --config /app/deploy/caddy/Caddyfile directory=/app/ -stdout_logfile=/data/log/nginx.log -stderr_logfile=/data/log/nginx.log +user=server +; server 是 --no-create-home 的系统账号,HOME 不存在时 Caddy 找不到数据目录会启动失败。 +environment=HOME="/tmp",XDG_DATA_HOME="/tmp",XDG_CONFIG_HOME="/tmp" +stdout_logfile=/data/log/caddy.log +stderr_logfile=/data/log/caddy.log autostart=true autorestart=true startsecs=5