Docker+Nginx单副本零停机发布实现并解决文件访问 404
更新时间:2026年04月28日 09:06:27 作者:韩zj
本文提供了解决Docker容器文件隔离问题的方法,核心是Nginx和业务容器挂载相同的宿主机文件目录,通过Nginx重写规则去掉/api前缀匹配业务服务路径,同时发布时流量同步切换到临时副本,避免404和中断问题
按照上一篇文章部署以后,网页和接口访问正常,但是访问文件是404,目前是解决了,给出通用的解决方案。
所有敏感信息(IP / 域名 / 密码)均用占位符标识,你只需替换成自身环境值即可直接使用,专门解决「IP 访问文件正常、域名访问 404」「发布切换副本文件访问中断」问题。
一、核心问题复现
- 正常访问:http://xxx.xxx.xxx.xxx:8080/profile/upload/test.bin
- 异常访问:https://your-domain.com/api/profile/upload/test.bin(404)
- 核心原因:Docker 容器文件隔离 + Nginx 路径映射不匹配 + 发布流量切换未同步文件访问
二、可直接复制的配置模板(全量)
- docker-compose.yml(替换占位符即可用,根据自己实际情况修改)
version: "3.9"
services:
redis:
image: bitnami/redis:latest
hostname: redis
container_name: redis
restart: always
privileged: true
ports:
- 6379:6379
environment:
REDIS_PASSWORD: your-redis-password # 替换:你的Redis密码
TZ: Asia/Shanghai
volumes:
- type: volume
source: redis-data
target: /bitnami/redis/data
volume: { }
networks:
- app-web-net
nginx:
image: nginx:latest
container_name: nginx
restart: always
ports:
- 80:80
- 443:443
privileged: true
networks:
- app-web-net
volumes:
- /root/nginx/nginx.conf:/etc/nginx/nginx.conf
- /root/nginx/conf.d/:/etc/nginx/conf.d/
- /root/nginx/html/:/usr/share/nginx/html/
- /root/nginx/logs/:/var/log/nginx/
- /root/nginx/cert/:/etc/nginx/cert/
# 核心:挂载文件目录(替换:你的宿主机文件目录)
- /root/your-app/uploadPath/:/root/your-app/uploadPath/
# 主业务服务(8080端口)
app-main:
image: your-app-image:1.0.0 # 替换:你的业务镜像
restart: always
privileged: true
depends_on:
- redis
ports:
- "8080:8080"
environment:
MYSQL_USER_NAME: your-db-username # 替换:数据库用户名
MYSQL_PWD: your-db-password # 替换:数据库密码
MYSQL_DB_NAME: your-db-name # 替换:数据库名
MYSQL_HOST: xxx.xxx.xxx.xxx # 替换:数据库IP
MYSQL_PORT: 3306
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PWD: your-redis-password # 替换:Redis密码(和上方一致)
SERVER_PORT: 8080
UPLOAD_PATH: /home/your-app/uploadPath/ # 容器内文件目录
TZ: Asia/Shanghai
volumes:
# 挂载文件目录(和Nginx挂载的宿主机目录一致)
- /root/your-app/uploadPath/:/home/your-app/uploadPath/
networks:
- app-web-net
healthcheck:
test: ["CMD", "curl", "-f", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080/api/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 60s
# 临时副本(发布兜底,8081端口)
app-temp:
image: your-app-image:1.0.0 # 替换:和主服务镜像一致
restart: "no"
privileged: true
depends_on:
- redis
ports:
- "8081:8080"
environment:
MYSQL_USER_NAME: your-db-username # 替换:和主服务一致
MYSQL_PWD: your-db-password # 替换:和主服务一致
MYSQL_DB_NAME: your-db-name # 替换:和主服务一致
MYSQL_HOST: xxx.xxx.xxx.xxx # 替换:和主服务一致
MYSQL_PORT: 3306
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PWD: your-redis-password # 替换:和主服务一致
SERVER_PORT: 8080
UPLOAD_PATH: /home/your-app/uploadPath/
TZ: Asia/Shanghai
volumes:
- /root/your-app/uploadPath/:/home/your-app/uploadPath/
networks:
- app-web-net
healthcheck:
test: ["CMD", "curl", "-f", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080/api/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 60s
volumes:
redis-data:
name: app-redis-data
external: true
networks:
app-web-net:
driver: bridge
- Nginx 配置文件(conf.d/default.conf)
# ===================== 全局核心配置 =====================
resolver 127.0.0.11 valid=30s;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 1s;
# 主服务集群(发布切换核心)
upstream app-servers {
least_conn;
server app-main:8080 max_fails=1 fail_timeout=1s;
keepalive 32;
}
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
proxy_buffering on;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
tcp_nopush on;
tcp_nodelay on;
# ===================== 1. HTTP 跳转HTTPS =====================
server {
listen 80;
# 替换:你的服务器IP + 域名
server_name xxx.xxx.xxx.xxx your-domain.com;
return 301 https://$host$request_uri;
}
# ===================== 2. HTTPS 核心配置 =====================
server {
listen 443 ssl;
# 替换:你的域名
server_name your-domain.com;
# 替换:你的SSL证书路径
ssl_certificate /etc/nginx/cert/your-domain.com.pem;
ssl_certificate_key /etc/nginx/cert/your-domain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_tickets off;
ssl_session_timeout 1d;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_prefer_server_ciphers on;
# ===================== 解决文件404核心配置 =====================
# 固件/上传文件访问:/api/profile/upload/xxx → 代理到/app-main/profile/upload/xxx
location ^~ /api/profile/upload/ {
rewrite ^/api/profile/upload/(.*)$ /profile/upload/$1 break;
proxy_pass http://app-servers;
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;
add_header Access-Control-Allow-Origin *;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# 头像文件访问:/api/profile/avatar/xxx → 代理到/app-main/profile/avatar/xxx
location ^~ /api/profile/avatar/ {
rewrite ^/api/profile/avatar/(.*)$ /profile/avatar/$1 break;
proxy_pass http://app-servers;
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;
add_header Access-Control-Allow-Origin *;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# Swagger文档代理(如有)
location ~ ^/api/(swagger-ui|v3/api-docs|swagger-resources|webjars) {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://app-servers;
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 Accept-Encoding "";
sub_filter 'https://your-domain.com:443/api' 'https://your-domain.com/api';
sub_filter 'https://your-domain.com:443' 'https://your-domain.com/api';
sub_filter '"url":"https://your-domain.com"' '"url":"https://your-domain.com/api"';
sub_filter '"url": "https://your-domain.com"' '"url": "https://your-domain.com/api"';
sub_filter 'https://your-domain.com/' 'https://your-domain.com/api/';
sub_filter 'https://your-domain.com/api/api/' 'https://your-domain.com/api/';
sub_filter_once off;
sub_filter_types application/json text/html text/javascript;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
# 前端页面访问
location / {
root /usr/share/nginx/html/your-app; # 替换:你的前端文件目录
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 主API接口代理
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://app-servers;
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-Prefix /api;
proxy_set_header X-Forwarded-Port "";
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Accept-Encoding "";
sub_filter 'http://your-domain.com/profile/upload/' 'https://your-domain.com/api/profile/upload/';
sub_filter 'https://your-domain.com/profile/upload/' 'https://your-domain.com/api/profile/upload/';
sub_filter_once off;
sub_filter_types application/json;
}
# 下载路径代理
location /download {
rewrite ^/download/(.*)$ /profile/upload/$1 break;
proxy_pass http://app-servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
add_header Access-Control-Allow-Origin *;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# 安全头配置
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
- 零停机发布脚本(deploy_app.sh)
#!/bin/bash
set +e
# ===================== 替换以下占位符 =====================
MONITOR_URL="https://your-domain.com/api/app/checkServerStatus" # 你的健康检查接口
MAIN_CHECK_URL="http://xxx.xxx.xxx.xxx:8080/api/app/checkServerStatus" # 主服务健康检查
TEMP_CHECK_URL="http://xxx.xxx.xxx.xxx:8081/api/app/checkServerStatus" # 临时服务健康检查
NGINX_CONF="/root/nginx/conf.d/default.conf" # Nginx配置文件路径
MAX_RETRY=30 # 最大重试次数
RETRY_INTERVAL=5 # 重试间隔(秒)
# =========================================================
# 后台监控公网接口状态
start_monitor() {
echo -e "\033[36m=== 开始实时监控公网接口:${MONITOR_URL} ===\033[0m"
echo -e "\033[36m=============================================\033[0m"
total=0
failed=0
while true; do
current_time=$(date "+%Y-%m-%d %H:%M:%S.%3N")
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --insecure ${MONITOR_URL} 2>/dev/null)
total=$((total+1))
if [ "$HTTP_CODE" = "200" ]; then
echo -e "\033[32m[${current_time}] 公网接口访问成功 | 状态码:${HTTP_CODE}\033[0m"
else
failed=$((failed+1))
echo -e "\033[31m[${current_time}] 公网接口访问失败 | 状态码:${HTTP_CODE}\033[0m"
fi
sleep 1
done
}
# 检查端口接口是否就绪
check_port_ready() {
local url=$1
local desc=$2
echo -e "\033[36m=== 等待${desc}就绪(独立端口:${url})===\033[0m"
local retry=0
while [ $retry -lt $MAX_RETRY ]; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --insecure ${url} 2>/dev/null)
if [ "$HTTP_CODE" = "200" ]; then
echo -e "\033[32m✅ ${desc}就绪!(状态码:200)\033[0m"
return 0
fi
retry=$((retry+1))
echo -e "\033[33m⚠️ 重试${retry}/${MAX_RETRY}:${desc}端口返回${HTTP_CODE},等待${RETRY_INTERVAL}秒...\033[0m"
sleep ${RETRY_INTERVAL}
done
echo -e "\033[31m❌ 错误:${desc}启动超时(未返回200)\033[0m"
kill $MONITOR_PID >/dev/null 2>&1
exit 1
}
# 切换Nginx代理地址
switch_nginx_proxy() {
local target=$1
local desc=$2
echo -e "\033[36m\n=== 切换Nginx代理到${desc} ===\033[0m"
if [ "$target" = "temp" ]; then
sed -i 's|proxy_pass http://app-servers|proxy_pass http://app-temp:8080|g' ${NGINX_CONF}
else
sed -i 's|proxy_pass http://app-temp:8080|proxy_pass http://app-servers|g' ${NGINX_CONF}
fi
docker exec -it nginx nginx -s reload >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "\033[32m✅ Nginx代理已切换到${desc}!\033[0m"
else
echo -e "\033[33m⚠️ Nginx重载警告,手动执行:docker exec -it nginx nginx -s reload\033[0m"
fi
}
# ===================== 主流程 =====================
# 1. 启动后台监控
start_monitor &
MONITOR_PID=$!
# 2. 启动临时副本
echo -e "\033[36m\n1. 启动临时业务副本(8081端口)\033[0m"
docker compose up -d app-temp
# 3. 检查临时副本就绪
if ! check_port_ready "${TEMP_CHECK_URL}" "临时副本"; then
kill $MONITOR_PID >/dev/null 2>&1
exit 1
fi
# 4. 切换Nginx代理到临时副本
switch_nginx_proxy "temp" "临时副本"
# 5. 重启主副本
echo -e "\033[36m\n3. 重启主业务副本(8080端口)\033[0m"
docker compose up -d --no-deps --force-recreate app-main
# 6. 检查主副本就绪
if ! check_port_ready "${MAIN_CHECK_URL}" "主副本"; then
echo -e "\033[31m\n⚠️ 警告:主副本启动超时,临时副本仍在提供服务\033[0m"
echo -e "\033[31m手动关闭命令:docker compose stop app-temp && docker compose rm -f app-temp\033[0m"
kill $MONITOR_PID >/dev/null 2>&1
exit 2
fi
# 7. 切换Nginx代理回主副本
switch_nginx_proxy "main" "主副本"
# 8. 关闭临时副本
echo -e "\033[36m\n5. 关闭临时副本\033[0m"
docker compose stop app-temp
docker compose rm -f app-temp
# 9. 停止监控并输出结果
kill $MONITOR_PID >/dev/null 2>&1
sleep 1
echo -e "\033[32m\n=============================================\033[0m"
echo -e "\033[32m✅ 发布完成!零停机验证结果:\033[0m"
echo -e "\033[32m - 公网接口全程无502/中断\033[0m"
echo -e "\033[32m - 文件访问:https://your-domain.com/api/profile/upload/test.bin 正常\033[0m"
echo -e "\033[32m=============================================\033[0m"
exit 0
三、替换说明(必看)

四、404 问题解决核心逻辑(关键)
- 文件目录挂载:Nginx 容器和业务容器挂载同一个宿主机文件目录,解决 Docker 隔离导致的文件不可见;
- 路径重写:通过rewrite ^/api/profile/upload/(.*)$ /profile/upload/$1break去掉/api前缀,匹配业务服务原生文件路径;
- 代理优先:放弃 Nginx静态访问文件,统一代理到业务服务,复用业务的文件访问逻辑;
- 流量同步切换:发布脚本切换 Nginx代理地址时,文件访问请求会同步切换到临时副本,避免发布中断。
五、使用步骤
- 复制上述 3 个文件到服务器对应目录;
- 全局替换所有占位符为自身环境值;
- 赋予发布脚本执行权限:chmod +x deploy_app.sh;
- 启动基础服务:docker compose up -d redis nginx app-main;
- 验证文件访问:curl https://your-domain.com/api/profile/upload/test.bin;
- 零停机发布:./deploy_app.sh。
六、验证 404 问题是否解决
# 1. 验证域名访问文件 curl -I https://your-domain.com/api/profile/upload/test.bin # 2. 验证发布过程中文件访问不中断 ./deploy_app.sh # 执行发布脚本,同时另起终端持续curl文件地址
返回状态码200即表示 404 问题解决。
总结
- 核心解决 Docker 容器文件隔离问题:Nginx 和业务容器挂载相同宿主机文件目录;
- 统一路径规则:通过 Nginx重写去掉/api前缀,匹配业务服务原生文件路径;
- 发布时流量同步切换:文件访问请求随 Nginx 代理地址切换到临时副本,避免 404/ 中断。
到此这篇关于Docker+Nginx单副本零停机发布实现并解决文件访问 404的文章就介绍到这了,更多相关Docker Nginx单副本零停机发布内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
supervisor下的Dockerfile的多服务镜像封装操作
这篇文章主要介绍了supervisor下的Dockerfile的多服务镜像封装操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧2020-11-11
使用Docker部署Nacos并配置MySQL数据源的详细步骤
Nacos是阿里巴巴开源的服务发现、配置管理和服务管理平台,它提供了注册中心和配置中心的功能,能够轻松地管理微服务的注册与发现,以及动态配置的管理,这篇文章主要给大家介绍了关于使用Docker部署Nacos并配置MySQL数据源的超详细步骤,需要的朋友可以参考下2024-05-05
树莓派4b ubuntu19 server 安装docker-ce的安装步骤
这篇文章主要介绍了树莓派4b ubuntu19 server 安装docker-ce的安装步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2019-11-11


最新评论